diff --git a/.devcontainer/mobile/container-compose-overrides.yml b/.devcontainer/mobile/container-compose-overrides.yml index 62a97a01eb..9fc7486fea 100644 --- a/.devcontainer/mobile/container-compose-overrides.yml +++ b/.devcontainer/mobile/container-compose-overrides.yml @@ -4,6 +4,7 @@ services: target: dev-container-mobile environment: - IMMICH_SERVER_URL=http://127.0.0.1:2283/ + - IMMICH_MEDIA_LOCATION=/data volumes: !override # bind mount host to /workspaces/immich - ..:/workspaces/immich - cli_node_modules:/workspaces/immich/cli/node_modules @@ -11,8 +12,8 @@ services: - open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules - server_node_modules:/workspaces/immich/server/node_modules - web_node_modules:/workspaces/immich/web/node_modules - - ${UPLOAD_LOCATION}/photos:/workspaces/immich/server/upload - - ${UPLOAD_LOCATION}/photos/upload:/workspaces/immich/server/upload/upload + - ${UPLOAD_LOCATION}/photos:/data + - ${UPLOAD_LOCATION}/photos/upload:/data/upload - /etc/localtime:/etc/localtime:ro database: diff --git a/.devcontainer/server/container-common.sh b/.devcontainer/server/container-common.sh index 34dcb6beac..544674e169 100755 --- a/.devcontainer/server/container-common.sh +++ b/.devcontainer/server/container-common.sh @@ -73,10 +73,8 @@ install_dependencies() { log "Installing dependencies" ( cd "${IMMICH_WORKSPACE}" || exit 1 - run_cmd make install-server - run_cmd make install-sdk - run_cmd make build-sdk - run_cmd make install-web + export CI=1 FROZEN=1 OFFLINE=1 + run_cmd make setup-web-dev setup-server-dev ) log "" } diff --git a/.devcontainer/server/container-compose-overrides.yml b/.devcontainer/server/container-compose-overrides.yml index eb8e66a3d3..951d763b4b 100644 --- a/.devcontainer/server/container-compose-overrides.yml +++ b/.devcontainer/server/container-compose-overrides.yml @@ -3,8 +3,10 @@ services: build: target: dev-container-server env_file: !reset [] + hostname: immich-dev environment: - IMMICH_SERVER_URL=http://127.0.0.1:2283/ + - IMMICH_MEDIA_LOCATION=/data volumes: !override - ..:/workspaces/immich - cli_node_modules:/workspaces/immich/cli/node_modules @@ -12,8 +14,8 @@ services: - open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules - server_node_modules:/workspaces/immich/server/node_modules - web_node_modules:/workspaces/immich/web/node_modules - - ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/workspaces/immich/server/upload - - ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/workspaces/immich/server/upload/upload + - ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data + - ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/data/upload - /etc/localtime:/etc/localtime:ro immich-web: @@ -21,7 +23,7 @@ services: immich-machine-learning: env_file: !reset [] - + database: env_file: !reset [] environment: !override @@ -30,7 +32,7 @@ services: POSTGRES_DB: ${DB_DATABASE_NAME-immich} POSTGRES_INITDB_ARGS: '--data-checksums' POSTGRES_HOST_AUTH_METHOD: md5 - volumes: + volumes: - ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data redis: diff --git a/.dockerignore b/.dockerignore index e182865ae0..f7efb5c56e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,33 +1,41 @@ .vscode/ .github/ .git/ +.env* +*.log +*.tmp +*.temp + +**/Dockerfile +**/node_modules/ +**/.pnpm-store/ +**/dist/ +**/coverage/ +**/build/ design/ docker/ !docker/scripts + docs/ +!docs/package.json +!docs/package-lock.json + e2e/ +!e2e/package.json +!e2e/package-lock.json + fastlane/ machine-learning/ misc/ mobile/ -cli/coverage/ -cli/dist/ -cli/node_modules/ - open-api/typescript-sdk/build/ -open-api/typescript-sdk/node_modules/ +!open-api/typescript-sdk/package.json +!open-api/typescript-sdk/package-lock.json -server/coverage/ -server/node_modules/ server/upload/ server/src/queries -server/dist/ server/www/ -web/node_modules/ -web/coverage/ web/.svelte-kit -web/build/ -web/.env diff --git a/.gitattributes b/.gitattributes index 3d43ff20ed..e3fb061bbc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,6 +12,12 @@ mobile/lib/**/*.drift.dart linguist-generated=true mobile/drift_schemas/main/drift_schema_*.json -diff -merge mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true +mobile/lib/infrastructure/repositories/db.repository.steps.dart -diff -merge +mobile/lib/infrastructure/repositories/db.repository.steps.dart linguist-generated=true + +mobile/test/drift/main/generated/** -diff -merge +mobile/test/drift/main/generated/** linguist-generated=true + open-api/typescript-sdk/fetch-client.ts -diff -merge open-api/typescript-sdk/fetch-client.ts linguist-generated=true diff --git a/.github/.nvmrc b/.github/.nvmrc index 5b540673a8..7377d130ed 100644 --- a/.github/.nvmrc +++ b/.github/.nvmrc @@ -1 +1 @@ -22.16.0 +22.17.1 diff --git a/.github/.prettierignore b/.github/.prettierignore new file mode 100644 index 0000000000..cc41cea9b2 --- /dev/null +++ b/.github/.prettierignore @@ -0,0 +1,4 @@ +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/.github/package-lock.json b/.github/package-lock.json index 423661befb..bea1c66e46 100644 --- a/.github/package-lock.json +++ b/.github/package-lock.json @@ -9,9 +9,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 33912d687c..a048536b2f 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -58,7 +58,7 @@ jobs: contents: read # Skip when PR from a fork if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && needs.pre-job.outputs.should_run == 'true' }} - runs-on: macos-14 + runs-on: mich steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -66,24 +66,40 @@ jobs: ref: ${{ inputs.ref || github.sha }} persist-credentials: false + - name: Create the Keystore + env: + KEY_JKS: ${{ secrets.KEY_JKS }} + working-directory: ./mobile + run: printf "%s" $KEY_JKS | base64 -d > android/key.jks + - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: 'zulu' java-version: '17' - cache: 'gradle' + + - name: Restore Gradle Cache + id: cache-gradle-restore + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.android/sdk + mobile/android/.gradle + mobile/.dart_tool + key: build-mobile-gradle-${{ runner.os }}-main - name: Setup Flutter SDK - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 + uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 with: channel: 'stable' flutter-version-file: ./mobile/pubspec.yaml cache: true - - name: Create the Keystore - env: - KEY_JKS: ${{ secrets.KEY_JKS }} - working-directory: ./mobile - run: echo $KEY_JKS | base64 -d > android/key.jks + - name: Setup Android SDK + uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 + with: + packages: '' - name: Get Packages working-directory: ./mobile @@ -103,12 +119,30 @@ jobs: ALIAS: ${{ secrets.ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} + IS_MAIN: ${{ github.ref == 'refs/heads/main' }} run: | - flutter build apk --release - flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64 + if [[ $IS_MAIN == 'true' ]]; then + flutter build apk --release + flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64 + else + flutter build apk --debug --split-per-abi --target-platform android-arm64 + fi - name: Publish Android Artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: release-apk-signed path: mobile/build/app/outputs/flutter-apk/*.apk + + - name: Save Gradle Cache + id: cache-gradle-save + uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + if: github.ref == 'refs/heads/main' + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.android/sdk + mobile/android/.gradle + mobile/.dart_tool + key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }} diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 74f5970139..9729450a91 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -38,6 +38,9 @@ jobs: with: node-version-file: './cli/.nvmrc' registry-url: 'https://registry.npmjs.org' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + - name: Prepare SDK run: npm ci --prefix ../open-api/typescript-sdk/ - name: Build SDK @@ -67,7 +70,7 @@ jobs: uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Login to GitHub Container Registry uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 @@ -96,7 +99,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index fb6d686878..6f1e68afce 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -63,7 +63,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -76,6 +76,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 20da0c5201..03f74dd7a3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -131,7 +131,7 @@ jobs: tag-suffix: '-rocm' platforms: linux/amd64 runner-mapping: '{"linux/amd64": "mich"}' - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0 + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1 permissions: contents: read actions: read @@ -154,7 +154,7 @@ jobs: name: Build and Push Server needs: pre-job if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0 + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1 permissions: contents: read actions: read @@ -177,7 +177,7 @@ jobs: runs-on: ubuntu-latest if: always() steps: - - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3 + - uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 with: needs: ${{ toJSON(needs) }} @@ -188,6 +188,6 @@ jobs: runs-on: ubuntu-latest if: always() steps: - - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3 + - uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 with: needs: ${{ toJSON(needs) }} diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 32010728cf..93b6c8ad04 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -57,6 +57,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './docs/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Run npm install run: npm ci diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml index 7a90747c12..7ef80306ba 100644 --- a/.github/workflows/fix-format.yml +++ b/.github/workflows/fix-format.yml @@ -32,6 +32,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './server/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Fix formatting run: make install-all && make format-all diff --git a/.github/workflows/org-checks.yml b/.github/workflows/org-checks.yml new file mode 100644 index 0000000000..9781dc3b83 --- /dev/null +++ b/.github/workflows/org-checks.yml @@ -0,0 +1,13 @@ +name: Org Checks + +on: + pull_request_review: + pull_request: + +jobs: + check-approvals: + name: Check for Team/Admin Review + uses: immich-app/devtools/.github/workflows/required-approval.yml@main + permissions: + pull-requests: read + contents: read diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml index 19f90143e0..2c75be8653 100644 --- a/.github/workflows/pr-label-validation.yml +++ b/.github/workflows/pr-label-validation.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - name: Require PR to have a changelog label - uses: mheap/github-action-required-labels@fb29a14a076b0f74099f6198f77750e8fc236016 # v5.5.0 + uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5.1 with: mode: exactly count: 1 diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index f1995bb866..fa1152c336 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -100,7 +100,7 @@ jobs: name: release-apk-signed - name: Create draft release - uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 with: draft: true tag_name: ${{ env.IMMICH_VERSION }} diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index bb3ae8f27f..c94ee14209 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -25,6 +25,8 @@ jobs: with: node-version-file: './open-api/typescript-sdk/.nvmrc' registry-url: 'https://registry.npmjs.org' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Install deps run: npm ci - name: Build diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 62e84ac957..573c908526 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -42,6 +42,9 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + defaults: + run: + working-directory: ./mobile steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -49,34 +52,29 @@ jobs: persist-credentials: false - name: Setup Flutter SDK - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 + uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 with: channel: 'stable' flutter-version-file: ./mobile/pubspec.yaml - name: Install dependencies run: dart pub get - working-directory: ./mobile - name: Install DCM - run: | - sudo apt-get update - wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg - echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list - sudo apt-get update - sudo apt-get install dcm + uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + version: auto + working-directory: ./mobile - name: Generate translation file run: make translation - working-directory: ./mobile - name: Run Build Runner run: make build - working-directory: ./mobile - name: Generate platform API run: make pigeon - working-directory: ./mobile - name: Find file changes uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 @@ -92,25 +90,22 @@ jobs: env: CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }} run: | - echo "ERROR: Generated files not up to date! Run make_build inside the mobile directory" + echo "ERROR: Generated files not up to date! Run 'make build' and 'make pigeon' inside the mobile directory" echo "Changed files: ${CHANGED_FILES}" exit 1 - name: Run dart analyze run: dart analyze --fatal-infos - working-directory: ./mobile - name: Run dart format - run: dart format lib/ --set-exit-if-changed - working-directory: ./mobile + run: make format - name: Run dart custom_lint run: dart run custom_lint - working-directory: ./mobile + # TODO: Use https://github.com/CQLabs/dcm-action - name: Run DCM - run: dcm analyze lib - working-directory: ./mobile + run: dcm analyze lib --fatal-style --fatal-warnings zizmor: name: zizmor @@ -134,7 +129,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: sarif_file: results.sarif category: zizmor diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b16713696..47a11c8232 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,6 +84,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './server/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Run npm install run: npm ci @@ -125,6 +127,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './cli/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Setup typescript-sdk run: npm ci && npm run build @@ -170,6 +174,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './cli/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Setup typescript-sdk run: npm ci && npm run build @@ -208,6 +214,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './web/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Run setup typescript-sdk run: npm ci && npm run build @@ -249,6 +257,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './web/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Run setup typescript-sdk run: npm ci && npm run build @@ -282,6 +292,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './web/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Install dependencies run: npm --prefix=web ci @@ -326,6 +338,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './e2e/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Run setup typescript-sdk run: npm ci && npm run build @@ -369,6 +383,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './server/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Run npm install run: npm ci @@ -402,6 +418,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './e2e/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Run setup typescript-sdk run: npm ci && npm run build @@ -450,6 +468,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './e2e/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Run setup typescript-sdk run: npm ci && npm run build @@ -461,7 +481,7 @@ jobs: if: ${{ !cancelled() }} - name: Install Playwright Browsers - run: npx playwright install --with-deps chromium + run: npx playwright install chromium --only-shell if: ${{ !cancelled() }} - name: Docker build @@ -479,7 +499,7 @@ jobs: runs-on: ubuntu-latest if: always() steps: - - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3 + - uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 with: needs: ${{ toJSON(needs) }} @@ -496,7 +516,7 @@ jobs: persist-credentials: false - name: Setup Flutter SDK - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 + uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 with: channel: 'stable' flutter-version-file: ./mobile/pubspec.yaml @@ -568,6 +588,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './.github/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Run npm install run: npm ci @@ -587,7 +609,7 @@ jobs: persist-credentials: false - name: Run ShellCheck - uses: ludeeus/action-shellcheck@master + uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 with: ignore_paths: >- **/open-api/** @@ -609,6 +631,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './server/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Install server dependencies run: npm --prefix=server ci @@ -644,7 +668,7 @@ jobs: contents: read services: postgres: - image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3 + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:1f5583fe3397210a0fbc7f11b0cec18bacc4a99e3e8ea0548e9bd6bcf26ec37a env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres @@ -670,6 +694,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './server/.nvmrc' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - name: Install server dependencies run: npm ci diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index 0758ef4dc9..084e1a97d6 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -52,6 +52,6 @@ jobs: permissions: {} if: always() steps: - - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3 + - uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 with: needs: ${{ toJSON(needs) }} diff --git a/.gitignore b/.gitignore index b4ebd04841..af85d96c02 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ mobile/android/fastlane/report.xml mobile/ios/fastlane/report.xml vite.config.js.timestamp-* +.pnpm-store diff --git a/Makefile b/Makefile index 1e7760ae68..815d1a153b 100644 --- a/Makefile +++ b/Makefile @@ -1,27 +1,33 @@ dev: - docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans || make dev-down + @trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans dev-down: docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans dev-update: - docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans + @trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans dev-scale: - docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans + @trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans .PHONY: e2e e2e: - docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans + @trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans + +e2e-update: + @trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans + +e2e-down: + docker compose -f ./e2e/docker-compose.yml down --remove-orphans prod: - docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans + @trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans prod-down: docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans prod-scale: - docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans + @trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans .PHONY: open-api open-api: @@ -48,6 +54,8 @@ audit-%: npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix install-%: npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i +ci-%: + npm --prefix $(subst sdk,open-api/typescript-sdk,$*) ci build-cli: build-sdk build-web: build-sdk build-%: install-% @@ -82,6 +90,7 @@ test-medium-dev: build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ; install-all: $(foreach M,$(MODULES),install-$M) ; +ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ; check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ; lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ; format-all: $(foreach M,$(filter-out sdk,$(MODULES)),format-$M) ; @@ -90,9 +99,12 @@ hygiene-all: lint-all format-all check-all sql audit-all; test-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),test-$M) ; clean: - find . -name "node_modules" -type d -prune -exec rm -rf '{}' + + find . -name "node_modules" -type d -prune -exec rm -rf {} + find . -name "dist" -type d -prune -exec rm -rf '{}' + find . -name "build" -type d -prune -exec rm -rf '{}' + find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' + - docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true - docker compose -f ./e2e/docker-compose.yml rm -v -f || true + command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true + command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true + +setup-server-dev: install-server +setup-web-dev: install-sdk build-sdk install-web diff --git a/cli/.nvmrc b/cli/.nvmrc index 5b540673a8..7377d130ed 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -22.16.0 +22.17.1 diff --git a/cli/bin/immich b/cli/bin/immich new file mode 100755 index 0000000000..924fff1230 --- /dev/null +++ b/cli/bin/immich @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import '../dist/index.js'; diff --git a/cli/package-lock.json b/cli/package-lock.json index e33b7d6cbe..3cd4da0480 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.72", + "version": "2.2.73", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.72", + "version": "2.2.73", "license": "GNU Affero General Public License version 3", "dependencies": { "chokidar": "^4.0.3", @@ -16,7 +16,7 @@ "micromatch": "^4.0.8" }, "bin": { - "immich": "dist/index.js" + "immich": "bin/immich" }, "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -27,7 +27,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.15.31", + "@types/node": "^22.16.4", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -42,7 +42,7 @@ "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "typescript-eslint": "^8.28.0", - "vite": "^6.0.0", + "vite": "^7.0.0", "vite-tsconfig-paths": "^5.0.0", "vitest": "^3.0.0", "vitest-fetch-mock": "^0.4.0", @@ -54,14 +54,14 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.135.3", + "version": "1.136.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.15.31", + "@types/node": "^22.16.4", "typescript": "^5.3.3" } }, @@ -607,9 +607,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -622,9 +622,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -682,9 +682,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { @@ -1276,6 +1276,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/cli-progress": { "version": "3.11.6", "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz", @@ -1286,6 +1296,13 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -1338,9 +1355,9 @@ } }, "node_modules/@types/node": { - "version": "22.15.32", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz", - "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==", + "version": "22.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", + "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1348,17 +1365,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", + "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/type-utils": "8.37.0", + "@typescript-eslint/utils": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1372,7 +1389,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.37.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -1388,16 +1405,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", + "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4" }, "engines": { @@ -1412,15 +1429,37 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", + "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/tsconfig-utils": "^8.37.0", + "@typescript-eslint/types": "^8.37.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", + "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1430,15 +1469,33 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", + "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", + "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1455,9 +1512,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz", + "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==", "dev": true, "license": "MIT", "engines": { @@ -1469,14 +1526,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", + "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.37.0", + "@typescript-eslint/tsconfig-utils": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1496,9 +1555,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1522,16 +1581,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz", + "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1546,14 +1605,14 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", + "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.37.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1564,15 +1623,16 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz", - "integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "debug": "^4.4.0", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", @@ -1587,8 +1647,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.1.4", - "vitest": "3.1.4" + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1597,14 +1657,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", - "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.4", - "@vitest/utils": "3.1.4", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -1613,13 +1674,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", - "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.4", + "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1628,7 +1689,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1640,9 +1701,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", - "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -1653,27 +1714,28 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", - "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.4", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", - "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.4", + "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -1682,27 +1744,27 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", - "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", - "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.4", - "loupe": "^3.1.3", + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { @@ -1710,9 +1772,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1792,6 +1854,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", + "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2105,9 +2179,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2232,19 +2306,19 @@ } }, "node_modules/eslint": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", - "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.27.0", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2256,9 +2330,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2309,14 +2383,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz", - "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", + "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.0" + "synckit": "^0.11.7" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -2402,9 +2476,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2419,9 +2493,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2431,16 +2505,29 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2749,9 +2836,9 @@ } }, "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -2975,6 +3062,13 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3076,9 +3170,9 @@ "license": "MIT" }, "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", "dev": true, "license": "MIT" }, @@ -3347,9 +3441,9 @@ "license": "MIT" }, "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { @@ -3386,9 +3480,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -3406,7 +3500,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3425,9 +3519,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -3799,6 +3893,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3813,14 +3920,13 @@ } }, "node_modules/synckit": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", - "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.3", - "tslib": "^2.8.1" + "@pkgr/core": "^0.2.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3885,9 +3991,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3930,9 +4036,9 @@ } }, "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -3950,9 +4056,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, "license": "MIT", "engines": { @@ -4005,13 +4111,6 @@ } } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4040,15 +4139,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz", + "integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.37.0", + "@typescript-eslint/parser": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4111,24 +4211,24 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", + "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.4", + "fdir": "^6.4.6", "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -4137,14 +4237,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -4186,17 +4286,17 @@ } }, "node_modules/vite-node": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", - "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.4.0", + "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -4229,9 +4329,9 @@ } }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4244,9 +4344,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -4257,32 +4357,34 @@ } }, "node_modules/vitest": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", - "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.4", - "@vitest/mocker": "3.1.4", - "@vitest/pretty-format": "^3.1.4", - "@vitest/runner": "3.1.4", - "@vitest/snapshot": "3.1.4", - "@vitest/spy": "3.1.4", - "@vitest/utils": "3.1.4", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", - "debug": "^4.4.0", + "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", + "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.4", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -4298,8 +4400,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.4", - "@vitest/ui": "3.1.4", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, @@ -4340,6 +4442,19 @@ "vitest": ">=2.0.0" } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/cli/package.json b/cli/package.json index b53ef1d530..6475a91465 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,11 +1,11 @@ { "name": "@immich/cli", - "version": "2.2.72", + "version": "2.2.73", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", "bin": { - "immich": "dist/index.js" + "immich": "./bin/immich" }, "license": "GNU Affero General Public License version 3", "keywords": [ @@ -21,7 +21,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.15.31", + "@types/node": "^22.16.4", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -36,7 +36,7 @@ "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "typescript-eslint": "^8.28.0", - "vite": "^6.0.0", + "vite": "^7.0.0", "vite-tsconfig-paths": "^5.0.0", "vitest": "^3.0.0", "vitest-fetch-mock": "^0.4.0", @@ -69,6 +69,6 @@ "micromatch": "^4.0.8" }, "volta": { - "node": "22.16.0" + "node": "22.17.1" } } diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index cb2348945b..6db9623b1a 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -16,7 +16,7 @@ name: immich-dev services: immich-server: container_name: immich_server - command: ['/usr/src/app/bin/immich-dev'] + command: ['immich-dev'] image: immich-server-dev:latest # extends: # file: hwaccel.transcoding.yml @@ -27,15 +27,16 @@ services: target: dev restart: unless-stopped volumes: - - ../server:/usr/src/app - - ../open-api:/usr/src/open-api - - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - - ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload - - /usr/src/app/node_modules + - ../server:/usr/src/app/server + - ../open-api:/usr/src/app/open-api + - ${UPLOAD_LOCATION}/photos:/data + - ${UPLOAD_LOCATION}/photos/upload:/data/upload + - /usr/src/app/server/node_modules - /etc/localtime:/etc/localtime:ro env_file: - .env environment: + IMMICH_MEDIA_LOCATION: /data IMMICH_REPOSITORY: immich-app/immich IMMICH_REPOSITORY_URL: https://github.com/immich-app/immich IMMICH_SOURCE_REF: local @@ -69,19 +70,20 @@ services: # Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919 # user: 0:0 build: - context: ../web - command: ['/usr/src/app/bin/immich-web'] + context: ../ + dockerfile: web/Dockerfile + command: ['immich-web'] env_file: - .env ports: - 3000:3000 - 24678:24678 volumes: - - ../web:/usr/src/app - - ../i18n:/usr/src/i18n - - ../open-api/:/usr/src/open-api/ + - ../web:/usr/src/app/web + - ../i18n:/usr/src/app/i18n + - ../open-api/:/usr/src/app/open-api/ # - ../../ui:/usr/ui - - /usr/src/app/node_modules + - /usr/src/app/web/node_modules ulimits: nofile: soft: 1048576 @@ -116,13 +118,13 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177 + image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11 healthcheck: test: redis-cli ping || exit 1 database: container_name: immich_postgres - image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:5f6a838e4e44c8e0e019d0ebfe3ee8952b69afc2809b2c25f7b0119641978e91 env_file: - .env environment: @@ -134,6 +136,7 @@ services: - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data ports: - 5432:5432 + shm_size: 128mb # set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics # immich-prometheus: # container_name: immich_prometheus diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index f31f651b18..fdeb6ef7a8 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -19,8 +19,10 @@ services: build: context: ../ dockerfile: server/Dockerfile + environment: + - IMMICH_MEDIA_LOCATION=/data volumes: - - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload + - ${UPLOAD_LOCATION}/photos:/data - /etc/localtime:/etc/localtime:ro env_file: - .env @@ -56,14 +58,14 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177 + image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11 healthcheck: test: redis-cli ping || exit 1 restart: always database: container_name: immich_postgres - image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:5f6a838e4e44c8e0e019d0ebfe3ee8952b69afc2809b2c25f7b0119641978e91 env_file: - .env environment: @@ -75,6 +77,7 @@ services: - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data ports: - 5432:5432 + shm_size: 128mb restart: always # set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics @@ -82,7 +85,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:9abc6cf6aea7710d163dbb28d8eeb7dc5baef01e38fa4cd146a406dd9f07f70d + image: prom/prometheus@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus @@ -94,7 +97,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:12.0.1-ubuntu@sha256:65575bb9c761335e2ff30e364f21d38632e3b2e75f5f81d83cc92f44b9bbc055 + image: grafana/grafana:12.0.2-ubuntu@sha256:0512d81cdeaaff0e370a9aa66027b465d1f1f04379c3a9c801a905fabbdbc7a5 volumes: - grafana-data:/var/lib/grafana diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a578167e68..6af0e0aa2a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -49,14 +49,14 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177 + image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11 healthcheck: test: redis-cli ping || exit 1 restart: always database: container_name: immich_postgres - image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:5f6a838e4e44c8e0e019d0ebfe3ee8952b69afc2809b2c25f7b0119641978e91 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USERNAME} @@ -67,6 +67,7 @@ services: volumes: # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file - ${DB_DATA_LOCATION}:/var/lib/postgresql/data + shm_size: 128mb restart: always volumes: diff --git a/docs/.nvmrc b/docs/.nvmrc index 5b540673a8..7377d130ed 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -22.16.0 +22.17.1 diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 24aa8e5d1b..9e14d10a0e 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -490,7 +490,7 @@ You can also scan the Postgres database file structure for errors:
Scan for file structure errors ```bash -docker exec -it immich_postgres pg_amcheck --username=postgres --heapallindexed --parent-check --rootdescend --progress --all --install-missing +docker exec -it immich_postgres pg_amcheck --username= --heapallindexed --parent-check --rootdescend --progress --all --install-missing ``` A normal result will end something like this and return with an exit code of `0`: diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 7b90c3b19a..deeefa5635 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -57,7 +57,7 @@ Then please follow the steps in the following section for restoring the database ```bash title='Backup' -docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | gzip > "/path/to/backup/dump.sql.gz" +docker exec -t immich_postgres pg_dumpall --clean --if-exists --username= | gzip > "/path/to/backup/dump.sql.gz" ``` ```bash title='Restore' @@ -79,7 +79,7 @@ docker compose up -d # Start remainder of Immich apps ```powershell title='Backup' -[System.IO.File]::WriteAllLines("C:\absolute\path\to\backup\dump.sql", (docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres)) +[System.IO.File]::WriteAllLines("C:\absolute\path\to\backup\dump.sql", (docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=)) ``` ```powershell title='Restore' @@ -150,12 +150,10 @@ for more info read the [release notes](https://github.com/immich-app/immich/rele - Preview images (small thumbnails and large previews) for each asset and thumbnails for recognized faces. - Stored in `UPLOAD_LOCATION/thumbs/`. - **Encoded Assets:** - - Videos that have been re-encoded from the original for wider compatibility. The original is not removed. - Stored in `UPLOAD_LOCATION/encoded-video/`. - **Postgres** - - The Immich database containing all the information to allow the system to function properly. **Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version. - Stored in `DB_DATA_LOCATION`. @@ -201,7 +199,6 @@ When you turn off the storage template engine, it will leave the assets in `UPLO - Temporarily located in `UPLOAD_LOCATION/upload/`. - Transferred to `UPLOAD_LOCATION/library/` upon successful upload. - **Postgres** - - The Immich database containing all the information to allow the system to function properly. **Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version. - Stored in `DB_DATA_LOCATION`. diff --git a/docs/docs/administration/img/admin-nightly-tasks.webp b/docs/docs/administration/img/admin-nightly-tasks.webp new file mode 100644 index 0000000000..b3d8f13cb6 Binary files /dev/null and b/docs/docs/administration/img/admin-nightly-tasks.webp differ diff --git a/docs/docs/administration/jobs-workers.md b/docs/docs/administration/jobs-workers.md index 75f97de982..4634151b9a 100644 --- a/docs/docs/administration/jobs-workers.md +++ b/docs/docs/administration/jobs-workers.md @@ -46,6 +46,12 @@ services: When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page. -Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed. - + +Additionally, some jobs (such as memories generation) run on a schedule, which is every night at midnight by default. To change when they run or enable/disable a job navigate to System Settings -> [Nightly Tasks Settings](https://my.immich.app/admin/system-settings?isOpen=nightly-tasks). + + + +:::note +Some jobs ([External Libraries](/docs/features/libraries) scanning, Database Dump) are configured in their own sections in System Settings. +::: diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index b60b5dbb8b..833b70f77a 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -20,7 +20,6 @@ Immich supports 3rd party authentication via [OpenID Connect][oidc] (OIDC), an i Before enabling OAuth in Immich, a new client application needs to be configured in the 3rd-party authentication server. While the specifics of this setup vary from provider to provider, the general approach should be the same. 1. Create a new (Client) Application - 1. The **Provider** type should be `OpenID Connect` or `OAuth2` 2. The **Client type** should be `Confidential` 3. The **Application** type should be `Web` @@ -29,7 +28,6 @@ Before enabling OAuth in Immich, a new client application needs to be configured 2. Configure Redirect URIs/Origins The **Sign-in redirect URIs** should include: - - `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx) - `http://DOMAIN:PORT/auth/login` - for logging in with OAuth from the Web Client - `http://DOMAIN:PORT/user-settings` - for manually linking OAuth in the Web Client @@ -37,21 +35,17 @@ Before enabling OAuth in Immich, a new client application needs to be configured Redirect URIs should contain all the domains you will be using to access Immich. Some examples include: Mobile - - `app.immich:///oauth-callback` (You **MUST** include this for iOS and Android mobile apps to work properly) Localhost - - `http://localhost:2283/auth/login` - `http://localhost:2283/user-settings` Local IP - - `http://192.168.0.200:2283/auth/login` - `http://192.168.0.200:2283/user-settings` Hostname - - `https://immich.example.com/auth/login` - `https://immich.example.com/user-settings` @@ -68,6 +62,7 @@ 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) | | Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) | | 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")**š** | | Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**š** | | Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) | | Button Text | string | Login with OAuth | Text for the OAuth button on the web | diff --git a/docs/docs/administration/server-commands.md b/docs/docs/administration/server-commands.md index b414f5deaa..b275d8fede 100644 --- a/docs/docs/administration/server-commands.md +++ b/docs/docs/administration/server-commands.md @@ -2,16 +2,17 @@ The `immich-server` docker image comes preinstalled with an administrative CLI (`immich-admin`) that supports the following commands: -| Command | Description | -| ------------------------ | ------------------------------------- | -| `help` | Display help | -| `reset-admin-password` | Reset the password for the admin user | -| `disable-password-login` | Disable password login | -| `enable-password-login` | Enable password login | -| `enable-oauth-login` | Enable OAuth login | -| `disable-oauth-login` | Disable OAuth login | -| `list-users` | List Immich users | -| `version` | Print Immich version | +| Command | Description | +| ------------------------ | ------------------------------------------------------------- | +| `help` | Display help | +| `reset-admin-password` | Reset the password for the admin user | +| `disable-password-login` | Disable password login | +| `enable-password-login` | Enable password login | +| `enable-oauth-login` | Enable OAuth login | +| `disable-oauth-login` | Disable OAuth login | +| `list-users` | List Immich users | +| `version` | Print Immich version | +| `change-media-location` | Change database file paths to align with a new media location | ## How to run a command @@ -88,3 +89,24 @@ Print Immich Version immich-admin version v1.129.0 ``` + +Change media location + +``` +immich-admin change-media-location +? Enter the previous value of IMMICH_MEDIA_LOCATION: /usr/src/app/upload +? Enter the new value of IMMICH_MEDIA_LOCATION: /data + + Previous value: /usr/src/app/upload + Current value: /data + + Changing database paths from "/usr/src/app/upload/*" to "/data/*" + +? Do you want to proceed? [Y/n] y + +Database file paths updated successfully! 🎉 + +You may now set IMMICH_MEDIA_LOCATION=/data and restart! + +(please remember to update applicable volume mounts e.g. ${UPLOAD_LOCATION}:/data) +``` diff --git a/docs/docs/developer/devcontainers.md b/docs/docs/developer/devcontainers.md index 10cb733383..c4c5396466 100644 --- a/docs/docs/developer/devcontainers.md +++ b/docs/docs/developer/devcontainers.md @@ -7,7 +7,7 @@ sidebar_position: 3 Dev Containers provide a consistent, reproducible development environment using Docker containers. With a single click, you can get started with an Immich development environment on Mac, Linux, Windows, or in the cloud using GitHub Codespaces. -[![Open in VSCode Containers](https://img.shields.io/static/v1?label=VSCode%20DevContainer&message=Immich&color=blue)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/immich-app/immich/) +Get started fast! [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/immich-app/immich/) @@ -71,7 +71,7 @@ cd immich The immich dev containers read environment variables from your shell environment, not from `.env` files. This allows them to work in cloud environments without pre-configuration. -:::important Required Configuration +:::important Configuration When running locally, and if you want to create (or use an existing) DB and/or photo storage folder, you must set the `UPLOAD_LOCATION` variable in your shell environment before launching the Dev Container. This determines where uploaded files are stored and also where the DB stores it data. ```bash @@ -88,6 +88,10 @@ source ~/.bashrc ### Step 3: Launch the Dev Container +:::tip +Immich development makes extensive use of specialized [base images](https://github.com/immich-app/base-images) for its docker-compose based development. For this reason, you won't be able to use VSCode's **_Clone Repository in a Container Volume_** command. +::: + #### Using VS Code UI: 1. Open the cloned repository in VS Code @@ -199,13 +203,11 @@ To use your SSH key for commit signing, see the [GitHub guide on SSH commit sign When the Dev Container starts, it automatically: 1. **Runs post-create script** (`container-server-post-create.sh`): - - Adjusts file permissions for the `node` user - Installs dependencies: `npm install` in all packages - Builds TypeScript SDK: `npm run build` in `open-api/typescript-sdk` 2. **Starts development servers** via VS Code tasks: - - `Immich API Server (Nest)` - API server with hot-reloading on port 2283 - `Immich Web Server (Vite)` - Web frontend with hot-reloading on port 3000 - Both servers watch for file changes and recompile automatically @@ -335,14 +337,12 @@ make install-all # Install all dependencies The Dev Container is pre-configured for debugging: 1. **API Server Debugging**: - - Set breakpoints in VS Code - Press `F5` or use "Run and Debug" panel - Select "Attach to Server" configuration - Debug port: 9231 2. **Worker Debugging**: - - Use "Attach to Workers" configuration - Debug port: 9230 @@ -428,7 +428,6 @@ While the Dev Container focuses on server and web development, you can connect m ``` 2. **Configure mobile app**: - - Server URL: `http://YOUR_IP:2283/api` - Ensure firewall allows port 2283 diff --git a/docs/docs/developer/pr-checklist.md b/docs/docs/developer/pr-checklist.md index 58581e669a..8fe3772b03 100644 --- a/docs/docs/developer/pr-checklist.md +++ b/docs/docs/developer/pr-checklist.md @@ -38,6 +38,19 @@ Run all server checks with `npm run check:all` You can use `npm run __:fix` to potentially correct some issues automatically for `npm run format` and `lint`. ::: +## Mobile Checks + +The following commands must be executed from within the mobile app directory of the codebase. + +- [ ] `make build` (auto-generate files using build_runner) +- [ ] `make analyze` (static analysis via Dart Analyzer and DCM) +- [ ] `make format` (formatting via Dart Formatter) +- [ ] `make test` (unit tests) + +:::info Auto Fix +You can use `dart fix --apply` and `dcm fix lib` to potentially correct some issues automatically for `make analyze`. +::: + ## OpenAPI The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/docs/developer/open-api.md) for more details. diff --git a/docs/docs/features/folder-view.md b/docs/docs/features/folder-view.md index 3534a22081..3d8613a042 100644 --- a/docs/docs/features/folder-view.md +++ b/docs/docs/features/folder-view.md @@ -2,7 +2,7 @@ Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template. -You can enable this feature under [`Account Settings > Features > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders) +You can enable this feature under [`Account Settings > Features > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders) ## Enable folder view diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index a8aa1316ed..ad76eb44b2 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -56,7 +56,7 @@ Internally, Immich uses the [glob](https://www.npmjs.com/package/glob) package t ### Automatic watching (EXPERIMENTAL) -This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. +This feature is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes. @@ -112,7 +112,7 @@ _Remember to run `docker compose up -d` to register the changes. Make sure you c These actions must be performed by the Immich administrator. -- Click on your avatar on the upper right corner +- Click on your avatar in the upper right corner - Click on Administration -> External Libraries - Click on Create an external libraryâ€Ļ - Select which user owns the library, this can not be changed later @@ -159,9 +159,7 @@ Within seconds, the assets from the old-pics and videos folders should show up i Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template. -You can enable this feature under [`Account Settings > Features > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders) - -The UI is currently only available for the web; mobile will come in a subsequent release. +You can enable this feature under [`Account Settings > Features > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders) @@ -171,7 +169,7 @@ The UI is currently only available for the web; mobile will come in a subsequent Only an admin can do this. ::: -You can define a custom interval for the trigger external library rescan under Administration -> Settings -> Library. +You can define a custom interval for the trigger external library rescan under Administration -> Settings -> External Library. You can set the scanning interval using the preset or cron format. For more information you can refer to [Crontab Guru](https://crontab.guru/). diff --git a/docs/docs/features/mobile-app.mdx b/docs/docs/features/mobile-app.mdx index 38222479b0..cd837741f1 100644 --- a/docs/docs/features/mobile-app.mdx +++ b/docs/docs/features/mobile-app.mdx @@ -88,9 +88,9 @@ It will only reflect files you add. ::: If the same asset is in more than one album it will only sync to the first album it's in, after that it won't sync again even if the user clicks sync albums manually. -To overcome this limitation, the files must be removed from the blacklist by +To overcome this limitation, the files must be removed from the ignore list by App settings -> Advanced -> Duplicate Assets -> Clear :::info -Cleaning duplicate assets from the list will cause all the previously uploaded duplicate files to be re-uploaded, the files will not actually be uploaded and will be rejected on the server side (due to duplication) but will be synchronized to the album and at the end will be added to the black list again at the end of the synchronization. +Cleaning duplicate assets from the list will cause all the previously uploaded duplicate files to be re-uploaded, the files will not actually be uploaded and will be rejected on the server side (due to duplication) but will be synchronized to the album and at the end will be added to the ignore list again at the end of the synchronization. ::: diff --git a/docs/docs/features/supported-formats.md b/docs/docs/features/supported-formats.md index e6fb2c8f00..16f1ab0b6b 100644 --- a/docs/docs/features/supported-formats.md +++ b/docs/docs/features/supported-formats.md @@ -16,7 +16,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a | `HEIC` | `.heic` | :white_check_mark: | | | `HEIF` | `.heif` | :white_check_mark: | | | `JPEG 2000` | `.jp2` | :white_check_mark: | | -| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | | +| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | | | `JPEG XL` | `.jxl` | :white_check_mark: | | | `PNG` | `.png` | :white_check_mark: | | | `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop | diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md index 209f673993..3ef538b1a0 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -17,22 +17,22 @@ The `"originalFileName"` column is the name of the file at time of upload, inclu ::: ```sql title="Find by original filename" -SELECT * FROM "assets" WHERE "originalFileName" = 'PXL_20230903_232542848.jpg'; -SELECT * FROM "assets" WHERE "originalFileName" LIKE 'PXL_%'; -- all files starting with PXL_ -SELECT * FROM "assets" WHERE "originalFileName" LIKE '%_2023_%'; -- all files with _2023_ in the middle +SELECT * FROM "asset" WHERE "originalFileName" = 'PXL_20230903_232542848.jpg'; +SELECT * FROM "asset" WHERE "originalFileName" LIKE 'PXL_%'; -- all files starting with PXL_ +SELECT * FROM "asset" WHERE "originalFileName" LIKE '%_2023_%'; -- all files with _2023_ in the middle ``` ```sql title="Find by path" -SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09-03/PXL_2023.jpg'; -SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%'; +SELECT * FROM "asset" WHERE "originalPath" = 'upload/library/admin/2023/2023-09-03/PXL_2023.jpg'; +SELECT * FROM "asset" WHERE "originalPath" LIKE 'upload/library/admin/2023/%'; ``` ```sql title="Find by ID" -SELECT * FROM "assets" WHERE "id" = '9f94e60f-65b6-47b7-ae44-a4df7b57f0e9'; +SELECT * FROM "asset" WHERE "id" = '9f94e60f-65b6-47b7-ae44-a4df7b57f0e9'; ``` ```sql title="Find by partial ID" -SELECT * FROM "assets" WHERE "id"::text LIKE '%ab431d3a%'; +SELECT * FROM "asset" WHERE "id"::text LIKE '%ab431d3a%'; ``` :::note @@ -40,60 +40,60 @@ You can calculate the checksum for a particular file by using the command `sha1s ::: ```sql title="Find by checksum (SHA-1)" -SELECT encode("checksum", 'hex') FROM "assets"; -SELECT * FROM "assets" WHERE "checksum" = decode('69de19c87658c4c15d9cacb9967b8e033bf74dd1', 'hex'); -SELECT * FROM "assets" WHERE "checksum" = '\x69de19c87658c4c15d9cacb9967b8e033bf74dd1'; -- alternate notation +SELECT encode("checksum", 'hex') FROM "asset"; +SELECT * FROM "asset" WHERE "checksum" = decode('69de19c87658c4c15d9cacb9967b8e033bf74dd1', 'hex'); +SELECT * FROM "asset" WHERE "checksum" = '\x69de19c87658c4c15d9cacb9967b8e033bf74dd1'; -- alternate notation ``` ```sql title="Find duplicate assets with identical checksum (SHA-1) (excluding trashed files)" -SELECT T1."checksum", array_agg(T2."id") ids FROM "assets" T1 - INNER JOIN "assets" T2 ON T1."checksum" = T2."checksum" AND T1."id" != T2."id" AND T2."deletedAt" IS NULL +SELECT T1."checksum", array_agg(T2."id") ids FROM "asset" T1 + INNER JOIN "asset" T2 ON T1."checksum" = T2."checksum" AND T1."id" != T2."id" AND T2."deletedAt" IS NULL WHERE T1."deletedAt" IS NULL GROUP BY T1."checksum"; ``` ```sql title="Live photos" -SELECT * FROM "assets" WHERE "livePhotoVideoId" IS NOT NULL; +SELECT * FROM "asset" WHERE "livePhotoVideoId" IS NOT NULL; ``` ```sql title="By description" -SELECT "assets".*, "exif"."description" FROM "exif" - JOIN "assets" ON "assets"."id" = "exif"."assetId" - WHERE TRIM("exif"."description") <> ''; -- all files with a description -SELECT "assets".*, "exif"."description" FROM "exif" - JOIN "assets" ON "assets"."id" = "exif"."assetId" - WHERE "exif"."description" ILIKE '%string to match%'; -- search by string +SELECT "asset".*, "asset_exif"."description" FROM "asset_exif" + JOIN "asset" ON "asset"."id" = "asset_exif"."assetId" + WHERE TRIM("asset_exif"."description") <> ''; -- all files with a description +SELECT "asset".*, "asset_exif"."description" FROM "asset_exif" + JOIN "asset" ON "asset"."id" = "asset_exif"."assetId" + WHERE "asset_exif"."description" ILIKE '%string to match%'; -- search by string ``` ```sql title="Without metadata" -SELECT "assets".* FROM "exif" - LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId" - WHERE "exif"."assetId" IS NULL; +SELECT "asset".* FROM "asset_exif" + LEFT JOIN "asset" ON "asset"."id" = "asset_exif"."assetId" + WHERE "asset_exif"."assetId" IS NULL; ``` ```sql title="size < 100,000 bytes, smallest to largest" -SELECT * FROM "assets" - JOIN "exif" ON "assets"."id" = "exif"."assetId" - WHERE "exif"."fileSizeInByte" < 100000 - ORDER BY "exif"."fileSizeInByte" ASC; +SELECT * FROM "asset" + JOIN "asset_exif" ON "asset"."id" = "asset_exif"."assetId" + WHERE "asset_exif"."fileSizeInByte" < 100000 + ORDER BY "asset_exif"."fileSizeInByte" ASC; ``` ```sql title="Without thumbnails" -SELECT * FROM "assets" WHERE "assets"."previewPath" IS NULL OR "assets"."thumbnailPath" IS NULL; +SELECT * FROM "asset" WHERE "asset"."previewPath" IS NULL OR "asset"."thumbnailPath" IS NULL; ``` ```sql title="By type" -SELECT * FROM "assets" WHERE "assets"."type" = 'VIDEO'; -SELECT * FROM "assets" WHERE "assets"."type" = 'IMAGE'; +SELECT * FROM "asset" WHERE "asset"."type" = 'VIDEO'; +SELECT * FROM "asset" WHERE "asset"."type" = 'IMAGE'; ``` ```sql title="Count by type" -SELECT "assets"."type", COUNT(*) FROM "assets" GROUP BY "assets"."type"; +SELECT "asset"."type", COUNT(1) FROM "asset" GROUP BY "asset"."type"; ``` ```sql title="Count by type (per user)" -SELECT "users"."email", "assets"."type", COUNT(*) FROM "assets" - JOIN "users" ON "assets"."ownerId" = "users"."id" - GROUP BY "assets"."type", "users"."email" ORDER BY "users"."email"; +SELECT "user"."email", "asset"."type", COUNT(1) FROM "asset" + JOIN "user" ON "asset"."ownerId" = "user"."id" + GROUP BY "asset"."type", "user"."email" ORDER BY "user"."email"; ``` ```sql title="Failed file movements" @@ -103,11 +103,11 @@ SELECT * FROM "move_history"; ## Users ```sql title="List all users" -SELECT * FROM "users"; +SELECT * FROM "user"; ``` ```sql title="Get owner info from asset ID" -SELECT "users".* FROM "users" JOIN "assets" ON "users"."id" = "assets"."ownerId" WHERE "assets"."id" = 'fa310b01-2f26-4b7a-9042-d578226e021f'; +SELECT "user".* FROM "user" JOIN "asset" ON "user"."id" = "asset"."ownerId" WHERE "asset"."id" = 'fa310b01-2f26-4b7a-9042-d578226e021f'; ``` ## System Config diff --git a/docs/docs/guides/external-library.md b/docs/docs/guides/external-library.md index 2ac917f930..3ad1679423 100644 --- a/docs/docs/guides/external-library.md +++ b/docs/docs/guides/external-library.md @@ -41,7 +41,7 @@ In the Immich web UI: - Click Add path -- Enter **/usr/src/app/external** as the path and click Add +- Enter **/home/user/photos1** as the path and click Add - Save the new path diff --git a/docs/docs/guides/template-backup-script.md b/docs/docs/guides/template-backup-script.md index dd0b94ebb1..34381dd0ee 100644 --- a/docs/docs/guides/template-backup-script.md +++ b/docs/docs/guides/template-backup-script.md @@ -52,9 +52,9 @@ REMOTE_BACKUP_PATH="/path/to/remote/backup/directory" ### Local # Backup Immich database -docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres > "$UPLOAD_LOCATION"/database-backup/immich-database.sql +docker exec -t immich_postgres pg_dumpall --clean --if-exists --username= > "$UPLOAD_LOCATION"/database-backup/immich-database.sql # For deduplicating backup programs such as Borg or Restic, compressing the content can increase backup size by making it harder to deduplicate. If you are using a different program or still prefer to compress, you can use the following command instead: -# docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | /usr/bin/gzip --rsyncable > "$UPLOAD_LOCATION"/database-backup/immich-database.sql.gz +# docker exec -t immich_postgres pg_dumpall --clean --if-exists --username= | /usr/bin/gzip --rsyncable > "$UPLOAD_LOCATION"/database-backup/immich-database.sql.gz ### Append to local Borg repository borg create "$BACKUP_PATH/immich-borg::{now}" "$UPLOAD_LOCATION" --exclude "$UPLOAD_LOCATION"/thumbs/ --exclude "$UPLOAD_LOCATION"/encoded-video/ diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index d3ca49a0a4..939c42439d 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -34,7 +34,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `TZ` | Timezone | \*1 | server | microservices | | `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | | `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | -| `IMMICH_MEDIA_LOCATION` | Media location inside the container âš ī¸**You probably shouldn't set this**\*2âš ī¸ | `./upload`\*3 | server | api, microservices | +| `IMMICH_MEDIA_LOCATION` | Media location inside the container âš ī¸**You probably shouldn't set this**\*2âš ī¸ | `/usr/src/app/upload` | server | api, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | | `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | | `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | | @@ -49,9 +49,6 @@ These environment variables are used by the `docker-compose.yml` file and do **N \*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead. -\*3: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`. -It only needs to be set if the Immich deployment method is changing. - ## Workers | Variable | Description | Default | Containers | @@ -72,22 +69,25 @@ Information on the current workers can be found [here](/docs/administration/jobs ## Database -| Variable | Description | Default | Containers | -| :---------------------------------- | :--------------------------------------------------------------------------- | :--------: | :----------------------------- | -| `DB_URL` | Database URL | | server | -| `DB_HOSTNAME` | Database host | `database` | server | -| `DB_PORT` | Database port | `5432` | server | -| `DB_USERNAME` | Database user | `postgres` | server, database\*1 | -| `DB_PASSWORD` | Database password | `postgres` | server, database\*1 | -| `DB_DATABASE_NAME` | Database name | `immich` | server, database\*1 | -| `DB_SSL_MODE` | Database SSL mode | | server | -| `DB_VECTOR_EXTENSION`\*2 | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server | -| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server | +| Variable | Description | Default | Containers | +| :---------------------------------- | :------------------------------------------------------------------------------------- | :--------: | :----------------------------- | +| `DB_URL` | Database URL | | server | +| `DB_HOSTNAME` | Database host | `database` | server | +| `DB_PORT` | Database port | `5432` | server | +| `DB_USERNAME` | Database user | `postgres` | server, database\*1 | +| `DB_PASSWORD` | Database password | `postgres` | server, database\*1 | +| `DB_DATABASE_NAME` | Database name | `immich` | server, database\*1 | +| `DB_SSL_MODE` | Database SSL mode | | server | +| `DB_VECTOR_EXTENSION`\*2 | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server | +| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server | +| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])\*3 | `SSD` | server | \*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`. \*2: If not provided, the appropriate extension to use is auto-detected at startup by introspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvecto.rs, pgvector. +\*3: Uses either [`postgresql.ssd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.ssd.conf) or [`postgresql.hdd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.hdd.conf) which mainly controls the Postgres `effective_io_concurrency` setting to allow for concurrenct IO on SSDs and sequential IO on HDDs. + :::info All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`. diff --git a/docs/docs/install/portainer.md b/docs/docs/install/portainer.md index 5ff324e282..916d89a0d5 100644 --- a/docs/docs/install/portainer.md +++ b/docs/docs/install/portainer.md @@ -39,8 +39,8 @@ alt="Dot Env Example" /> - Change the default `DB_PASSWORD`, and add custom database connection information if necessary. -- Change `DB_DATA_LOCATION` to a folder where the database will be saved to disk. -- Change `UPLOAD_LOCATION` to a folder where media (uploaded and generated) will be stored. +- Change `DB_DATA_LOCATION` to a folder (absolute path) where the database will be saved to disk. +- Change `UPLOAD_LOCATION` to a folder (absolute path) where media (uploaded and generated) will be stored. 11. Click on "**Deploy the stack**". diff --git a/docs/docs/install/unraid.md b/docs/docs/install/unraid.md index 344b912aea..efb493f267 100644 --- a/docs/docs/install/unraid.md +++ b/docs/docs/install/unraid.md @@ -75,7 +75,6 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich" 5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**" 6. Select the cog âš™ī¸ next to Immich, click "**Edit Stack**", then click "**Env File**" 7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following: - - `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION` - `DB_DATA_LOCATION`: Change this to use an Unraid share (preferably a cache pool, e.g. `/mnt/user/appdata/postgresql/data`). This uses the `appdata` share. Do also create the `postgresql` folder, by running `mkdir /mnt/user/{share_location}/postgresql/data`. If left at default it will try to use Unraid's `/boot/config/plugins/compose.manager/projects/[stack_name]/postgres` folder which it doesn't have permissions to, resulting in this container continuously restarting. diff --git a/docs/package-lock.json b/docs/package-lock.json index 602232da07..53ac997e3b 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8,8 +8,9 @@ "name": "documentation", "version": "0.0.0", "dependencies": { - "@docusaurus/core": "~3.7.0", - "@docusaurus/preset-classic": "~3.7.0", + "@docusaurus/core": "~3.8.0", + "@docusaurus/preset-classic": "~3.8.0", + "@docusaurus/theme-common": "~3.8.0", "@mdi/js": "^7.3.67", "@mdi/react": "^1.6.1", "@mdx-js/react": "^3.0.0", @@ -18,6 +19,7 @@ "clsx": "^2.0.0", "docusaurus-lunr-search": "^3.3.2", "docusaurus-preset-openapi": "^0.7.5", + "lunr": "^2.3.9", "postcss": "^8.4.25", "prism-react-renderer": "^2.3.1", "raw-loader": "^4.0.2", @@ -27,7 +29,7 @@ "url": "^0.11.0" }, "devDependencies": { - "@docusaurus/module-type-aliases": "~3.7.0", + "@docusaurus/module-type-aliases": "~3.8.0", "@docusaurus/tsconfig": "^3.7.0", "@docusaurus/types": "^3.7.0", "prettier": "^3.2.4", @@ -1980,9 +1982,9 @@ } }, "node_modules/@csstools/cascade-layer-name-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.4.tgz", - "integrity": "sha512-7DFHlPuIxviKYZrOiwVU/PiHLm3lLUR23OMuEEtfEOQTOp9hzQ2JjdY6X5H18RVuUPJqSCI+qNnD5iOLMVE0bA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", + "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", "funding": [ { "type": "github", @@ -1998,8 +2000,8 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/color-helpers": { @@ -2022,9 +2024,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz", - "integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", "funding": [ { "type": "github", @@ -2040,14 +2042,14 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz", - "integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", "funding": [ { "type": "github", @@ -2061,20 +2063,20 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^5.0.2", - "@csstools/css-calc": "^2.1.2" + "@csstools/css-calc": "^2.1.4" }, "engines": { "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", - "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "funding": [ { "type": "github", @@ -2090,13 +2092,13 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", - "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "funding": [ { "type": "github", @@ -2113,9 +2115,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz", - "integrity": "sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", "funding": [ { "type": "github", @@ -2131,14 +2133,14 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/postcss-cascade-layers": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.1.tgz", - "integrity": "sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", + "integrity": "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==", "funding": [ { "type": "github", @@ -2197,9 +2199,9 @@ } }, "node_modules/@csstools/postcss-color-function": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.8.tgz", - "integrity": "sha512-9dUvP2qpZI6PlGQ/sob+95B3u5u7nkYt9yhZFCC7G9HBRHBxj+QxS/wUlwaMGYW0waf+NIierI8aoDTssEdRYw==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.10.tgz", + "integrity": "sha512-4dY0NBu7NVIpzxZRgh/Q/0GPSz/jLSw0i/u3LTUor0BkQcz/fNhN10mSWBDsL0p9nDb0Ky1PD6/dcGbhACuFTQ==", "funding": [ { "type": "github", @@ -2212,10 +2214,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.8", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2226,9 +2228,9 @@ } }, "node_modules/@csstools/postcss-color-mix-function": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.8.tgz", - "integrity": "sha512-yuZpgWUzqZWQhEqfvtJufhl28DgO9sBwSbXbf/59gejNuvZcoUTRGQZhzhwF4ccqb53YAGB+u92z9+eSKoB4YA==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.10.tgz", + "integrity": "sha512-P0lIbQW9I4ShE7uBgZRib/lMTf9XMjJkFl/d6w4EMNHu2qvQ6zljJGEcBkw/NsBtq/6q3WrmgxSS8kHtPMkK4Q==", "funding": [ { "type": "github", @@ -2241,10 +2243,39 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.8", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.0.tgz", + "integrity": "sha512-Z5WhouTyD74dPFPrVE7KydgNS9VvnjB8qcdes9ARpCOItb4jTnm7cHp4FhxCRUoyhabD0WVv43wbkJ4p8hLAlQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2255,9 +2286,9 @@ } }, "node_modules/@csstools/postcss-content-alt-text": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.4.tgz", - "integrity": "sha512-YItlZUOuZJCBlRaCf8Aucc1lgN41qYGALMly0qQllrxYJhiyzlI6RxOTMUvtWk+KhS8GphMDsDhKQ7KTPfEMSw==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.6.tgz", + "integrity": "sha512-eRjLbOjblXq+byyaedQRSrAejKGNAFued+LcbzT+LCL78fabxHkxYjBbxkroONxHHYu2qxhFK2dBStTLPG3jpQ==", "funding": [ { "type": "github", @@ -2270,9 +2301,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2283,9 +2314,9 @@ } }, "node_modules/@csstools/postcss-exponential-functions": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.7.tgz", - "integrity": "sha512-XTb6Mw0v2qXtQYRW9d9duAjDnoTbBpsngD7sRNLmYDjvwU2ebpIHplyxgOeo6jp/Kr52gkLi5VaK5RDCqzMzZQ==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz", + "integrity": "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==", "funding": [ { "type": "github", @@ -2298,9 +2329,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.2", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -2336,9 +2367,9 @@ } }, "node_modules/@csstools/postcss-gamut-mapping": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.8.tgz", - "integrity": "sha512-/K8u9ZyGMGPjmwCSIjgaOLKfic2RIGdFHHes84XW5LnmrvdhOTVxo255NppHi3ROEvoHPW7MplMJgjZK5Q+TxA==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.10.tgz", + "integrity": "sha512-QDGqhJlvFnDlaPAfCYPsnwVA6ze+8hhrwevYWlnUeSjkkZfBpcCO42SaUD8jiLlq7niouyLgvup5lh+f1qessg==", "funding": [ { "type": "github", @@ -2351,9 +2382,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.8", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -2363,9 +2394,9 @@ } }, "node_modules/@csstools/postcss-gradients-interpolation-method": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.8.tgz", - "integrity": "sha512-CoHQ/0UXrvxLovu0ZeW6c3/20hjJ/QRg6lyXm3dZLY/JgvRU6bdbQZF/Du30A4TvowfcgvIHQmP1bNXUxgDrAw==", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.10.tgz", + "integrity": "sha512-HHPauB2k7Oits02tKFUeVFEU2ox/H3OQVrP3fSOKDxvloOikSal+3dzlyTZmYsb9FlY9p5EUpBtz0//XBmy+aw==", "funding": [ { "type": "github", @@ -2378,10 +2409,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.8", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2392,9 +2423,9 @@ } }, "node_modules/@csstools/postcss-hwb-function": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.8.tgz", - "integrity": "sha512-LpFKjX6hblpeqyych1cKmk+3FJZ19QmaJtqincySoMkbkG/w2tfbnO5oE6mlnCTXcGUJ0rCEuRHvTqKK0nHYUQ==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.10.tgz", + "integrity": "sha512-nOKKfp14SWcdEQ++S9/4TgRKchooLZL0TUFdun3nI4KPwCjETmhjta1QT4ICQcGVWQTvrsgMM/aLB5We+kMHhQ==", "funding": [ { "type": "github", @@ -2407,10 +2438,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.8", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2421,9 +2452,9 @@ } }, "node_modules/@csstools/postcss-ic-unit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.0.tgz", - "integrity": "sha512-9QT5TDGgx7wD3EEMN3BSUG6ckb6Eh5gSPT5kZoVtUuAonfPmLDJyPhqR4ntPpMYhUKAMVKAg3I/AgzqHMSeLhA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.2.tgz", + "integrity": "sha512-lrK2jjyZwh7DbxaNnIUjkeDmU8Y6KyzRBk91ZkI5h8nb1ykEfZrtIVArdIjX4DHMIBGpdHrgP0n4qXDr7OHaKA==", "funding": [ { "type": "github", @@ -2436,7 +2467,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, @@ -2470,9 +2501,9 @@ } }, "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.1.tgz", - "integrity": "sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", + "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", "funding": [ { "type": "github", @@ -2531,9 +2562,9 @@ } }, "node_modules/@csstools/postcss-light-dark-function": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.7.tgz", - "integrity": "sha512-ZZ0rwlanYKOHekyIPaU+sVm3BEHCe+Ha0/px+bmHe62n0Uc1lL34vbwrLYn6ote8PHlsqzKeTQdIejQCJ05tfw==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.9.tgz", + "integrity": "sha512-1tCZH5bla0EAkFAI2r0H33CDnIBeLUaJh1p+hvvsylJ4svsv2wOmJjJn+OXwUZLXef37GYbRIVKX+X+g6m+3CQ==", "funding": [ { "type": "github", @@ -2546,9 +2577,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2650,9 +2681,9 @@ } }, "node_modules/@csstools/postcss-logical-viewport-units": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.3.tgz", - "integrity": "sha512-OC1IlG/yoGJdi0Y+7duz/kU/beCwO+Gua01sD6GtOtLi7ByQUpcIqs7UE/xuRPay4cHgOMatWdnDdsIDjnWpPw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz", + "integrity": "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==", "funding": [ { "type": "github", @@ -2665,7 +2696,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-tokenizer": "^3.0.3", + "@csstools/css-tokenizer": "^3.0.4", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2676,9 +2707,9 @@ } }, "node_modules/@csstools/postcss-media-minmax": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.7.tgz", - "integrity": "sha512-LB6tIP7iBZb5CYv8iRenfBZmbaG3DWNEziOnPjGoQX5P94FBPvvTBy68b/d9NnS5PELKwFmmOYsAEIgEhDPCHA==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz", + "integrity": "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==", "funding": [ { "type": "github", @@ -2691,10 +2722,10 @@ ], "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.2", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/media-query-list-parser": "^4.0.2" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" }, "engines": { "node": ">=18" @@ -2704,9 +2735,9 @@ } }, "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.4.tgz", - "integrity": "sha512-AnGjVslHMm5xw9keusQYvjVWvuS7KWK+OJagaG0+m9QnIjZsrysD2kJP/tr/UJIyYtMCtu8OkUd+Rajb4DqtIQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz", + "integrity": "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==", "funding": [ { "type": "github", @@ -2719,9 +2750,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/media-query-list-parser": "^4.0.2" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" }, "engines": { "node": ">=18" @@ -2782,9 +2813,9 @@ } }, "node_modules/@csstools/postcss-oklab-function": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.8.tgz", - "integrity": "sha512-+5aPsNWgxohXoYNS1f+Ys0x3Qnfehgygv3qrPyv+Y25G0yX54/WlVB+IXprqBLOXHM1gsVF+QQSjlArhygna0Q==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.10.tgz", + "integrity": "sha512-ZzZUTDd0fgNdhv8UUjGCtObPD8LYxMH+MJsW9xlZaWTV8Ppr4PtxlHYNMmF4vVWGl0T6f8tyWAKjoI6vePSgAg==", "funding": [ { "type": "github", @@ -2797,10 +2828,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.8", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2811,9 +2842,9 @@ } }, "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.0.0.tgz", - "integrity": "sha512-XQPtROaQjomnvLUSy/bALTR5VCtTVUFwYs1SblvYgLSeTo2a/bMNwUwo2piXw5rTv/FEYiy5yPSXBqg9OKUx7Q==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.1.0.tgz", + "integrity": "sha512-YrkI9dx8U4R8Sz2EJaoeD9fI7s7kmeEBfmO+UURNeL6lQI7VxF6sBE+rSqdCBn4onwqmxFdBU3lTwyYb/lCmxA==", "funding": [ { "type": "github", @@ -2836,9 +2867,9 @@ } }, "node_modules/@csstools/postcss-random-function": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-1.0.3.tgz", - "integrity": "sha512-dbNeEEPHxAwfQJ3duRL5IPpuD77QAHtRl4bAHRs0vOVhVbHrsL7mHnwe0irYjbs9kYwhAHZBQTLBgmvufPuRkA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz", + "integrity": "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==", "funding": [ { "type": "github", @@ -2851,9 +2882,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.2", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -2863,9 +2894,9 @@ } }, "node_modules/@csstools/postcss-relative-color-syntax": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.8.tgz", - "integrity": "sha512-eGE31oLnJDoUysDdjS9MLxNZdtqqSxjDXMdISpLh80QMaYrKs7VINpid34tWQ+iU23Wg5x76qAzf1Q/SLLbZVg==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.10.tgz", + "integrity": "sha512-8+0kQbQGg9yYG8hv0dtEpOMLwB9M+P7PhacgIzVzJpixxV4Eq9AUQtQw8adMmAJU1RBBmIlpmtmm3XTRd/T00g==", "funding": [ { "type": "github", @@ -2878,10 +2909,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.8", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2930,9 +2961,9 @@ } }, "node_modules/@csstools/postcss-sign-functions": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.2.tgz", - "integrity": "sha512-4EcAvXTUPh7n6UoZZkCzgtCf/wPzMlTNuddcKg7HG8ozfQkUcHsJ2faQKeLmjyKdYPyOUn4YA7yDPf8K/jfIxw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", + "integrity": "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==", "funding": [ { "type": "github", @@ -2945,9 +2976,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.2", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -2957,9 +2988,9 @@ } }, "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.7.tgz", - "integrity": "sha512-rdrRCKRnWtj5FyRin0u/gLla7CIvZRw/zMGI1fVJP0Sg/m1WGicjPVHRANL++3HQtsiXKAbPrcPr+VkyGck0IA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz", + "integrity": "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==", "funding": [ { "type": "github", @@ -2972,9 +3003,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.2", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -3010,9 +3041,9 @@ } }, "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.7.tgz", - "integrity": "sha512-qTrZgLju3AV7Djhzuh2Bq/wjFqbcypnk0FhHjxW8DWJQcZLS1HecIus4X2/RLch1ukX7b+YYCdqbEnpIQO5ccg==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz", + "integrity": "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==", "funding": [ { "type": "github", @@ -3025,9 +3056,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.2", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -3128,9 +3159,9 @@ } }, "node_modules/@docusaurus/babel": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.7.0.tgz", - "integrity": "sha512-0H5uoJLm14S/oKV3Keihxvh8RV+vrid+6Gv+2qhuzbqHanawga8tYnsdpjEyt36ucJjqlby2/Md2ObWjA02UXQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.8.1.tgz", + "integrity": "sha512-3brkJrml8vUbn9aeoZUlJfsI/GqyFcDgQJwQkmBtclJgWDEQBKKeagZfOgx0WfUQhagL1sQLNW0iBdxnI863Uw==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.9", @@ -3143,8 +3174,8 @@ "@babel/runtime": "^7.25.9", "@babel/runtime-corejs3": "^7.25.9", "@babel/traverse": "^7.25.9", - "@docusaurus/logger": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@docusaurus/logger": "3.8.1", + "@docusaurus/utils": "3.8.1", "babel-plugin-dynamic-import-node": "^2.3.3", "fs-extra": "^11.1.1", "tslib": "^2.6.0" @@ -3154,31 +3185,30 @@ } }, "node_modules/@docusaurus/bundler": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.7.0.tgz", - "integrity": "sha512-CUUT9VlSGukrCU5ctZucykvgCISivct+cby28wJwCC/fkQFgAHRp/GKv2tx38ZmXb7nacrKzFTcp++f9txUYGg==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.8.1.tgz", + "integrity": "sha512-/z4V0FRoQ0GuSLToNjOSGsk6m2lQUG4FRn8goOVoZSRsTrU8YR2aJacX5K3RG18EaX9b+52pN4m1sL3MQZVsQA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.9", - "@docusaurus/babel": "3.7.0", - "@docusaurus/cssnano-preset": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@docusaurus/babel": "3.8.1", + "@docusaurus/cssnano-preset": "3.8.1", + "@docusaurus/logger": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", "babel-loader": "^9.2.1", - "clean-css": "^5.3.2", + "clean-css": "^5.3.3", "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.8.1", + "css-loader": "^6.11.0", "css-minimizer-webpack-plugin": "^5.0.1", "cssnano": "^6.1.2", "file-loader": "^6.2.0", "html-minifier-terser": "^7.2.0", - "mini-css-extract-plugin": "^2.9.1", + "mini-css-extract-plugin": "^2.9.2", "null-loader": "^4.0.1", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "postcss-preset-env": "^10.1.0", - "react-dev-utils": "^12.0.1", + "postcss": "^8.5.4", + "postcss-loader": "^7.3.4", + "postcss-preset-env": "^10.2.1", "terser-webpack-plugin": "^5.3.9", "tslib": "^2.6.0", "url-loader": "^4.1.1", @@ -3198,18 +3228,18 @@ } }, "node_modules/@docusaurus/core": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.7.0.tgz", - "integrity": "sha512-b0fUmaL+JbzDIQaamzpAFpTviiaU4cX3Qz8cuo14+HGBCwa0evEK0UYCBFY3n4cLzL8Op1BueeroUD2LYAIHbQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.8.1.tgz", + "integrity": "sha512-ENB01IyQSqI2FLtOzqSI3qxG2B/jP4gQPahl2C3XReiLebcVh5B5cB9KYFvdoOqOWPyr5gXK4sjgTKv7peXCrA==", "license": "MIT", "dependencies": { - "@docusaurus/babel": "3.7.0", - "@docusaurus/bundler": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/babel": "3.8.1", + "@docusaurus/bundler": "3.8.1", + "@docusaurus/logger": "3.8.1", + "@docusaurus/mdx-loader": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-common": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "boxen": "^6.2.1", "chalk": "^4.1.2", "chokidar": "^3.5.3", @@ -3217,19 +3247,19 @@ "combine-promises": "^1.1.0", "commander": "^5.1.0", "core-js": "^3.31.1", - "del": "^6.1.1", "detect-port": "^1.5.1", "escape-html": "^1.0.3", "eta": "^2.2.0", "eval": "^0.1.8", + "execa": "5.1.1", "fs-extra": "^11.1.1", "html-tags": "^3.3.1", "html-webpack-plugin": "^5.6.0", "leven": "^3.1.0", "lodash": "^4.17.21", + "open": "^8.4.0", "p-map": "^4.0.0", "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", "react-loadable-ssr-addon-v5-slorber": "^1.0.1", @@ -3238,7 +3268,7 @@ "react-router-dom": "^5.3.4", "semver": "^7.5.4", "serve-handler": "^6.1.6", - "shelljs": "^0.8.5", + "tinypool": "^1.0.2", "tslib": "^2.6.0", "update-notifier": "^6.0.2", "webpack": "^5.95.0", @@ -3259,13 +3289,13 @@ } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.7.0.tgz", - "integrity": "sha512-X9GYgruZBSOozg4w4dzv9uOz8oK/EpPVQXkp0MM6Tsgp/nRIU9hJzJ0Pxg1aRa3xCeEQTOimZHcocQFlLwYajQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.8.1.tgz", + "integrity": "sha512-G7WyR2N6SpyUotqhGznERBK+x84uyhfMQM2MmDLs88bw4Flom6TY46HzkRkSEzaP9j80MbTN8naiL1fR17WQug==", "license": "MIT", "dependencies": { "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", + "postcss": "^8.5.4", "postcss-sort-media-queries": "^5.2.0", "tslib": "^2.6.0" }, @@ -3274,9 +3304,9 @@ } }, "node_modules/@docusaurus/logger": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.7.0.tgz", - "integrity": "sha512-z7g62X7bYxCYmeNNuO9jmzxLQG95q9QxINCwpboVcNff3SJiHJbGrarxxOVMVmAh1MsrSfxWkVGv4P41ktnFsA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.8.1.tgz", + "integrity": "sha512-2wjeGDhKcExEmjX8k1N/MRDiPKXGF2Pg+df/bDDPnnJWHXnVEZxXj80d6jcxp1Gpnksl0hF8t/ZQw9elqj2+ww==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", @@ -3287,21 +3317,21 @@ } }, "node_modules/@docusaurus/mdx-loader": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.7.0.tgz", - "integrity": "sha512-OFBG6oMjZzc78/U3WNPSHs2W9ZJ723ewAcvVJaqS0VgyeUfmzUV8f1sv+iUHA0DtwiR5T5FjOxj6nzEE8LY6VA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.8.1.tgz", + "integrity": "sha512-DZRhagSFRcEq1cUtBMo4TKxSNo/W6/s44yhr8X+eoXqCLycFQUylebOMPseHi5tc4fkGJqwqpWJLz6JStU9L4w==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/logger": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "@mdx-js/mdx": "^3.0.0", "@slorber/remark-comment": "^1.0.0", "escape-html": "^1.0.3", "estree-util-value-to-estree": "^3.0.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", - "image-size": "^1.0.2", + "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-string": "^4.0.0", "rehype-raw": "^7.0.0", @@ -3326,17 +3356,17 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.7.0.tgz", - "integrity": "sha512-g7WdPqDNaqA60CmBrr0cORTrsOit77hbsTj7xE2l71YhBn79sxdm7WMK7wfhcaafkbpIh7jv5ef5TOpf1Xv9Lg==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.8.1.tgz", + "integrity": "sha512-6xhvAJiXzsaq3JdosS7wbRt/PwEPWHr9eM4YNYqVlbgG1hSK3uQDXTVvQktasp3VO6BmfYWPozueLWuj4gB+vg==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.7.0", + "@docusaurus/types": "3.8.1", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", "@types/react-router-dom": "*", - "react-helmet-async": "npm:@slorber/react-helmet-async@*", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" }, "peerDependencies": { @@ -3345,24 +3375,24 @@ } }, "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.7.0.tgz", - "integrity": "sha512-EFLgEz6tGHYWdPU0rK8tSscZwx+AsyuBW/r+tNig2kbccHYGUJmZtYN38GjAa3Fda4NU+6wqUO5kTXQSRBQD3g==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.8.1.tgz", + "integrity": "sha512-vNTpMmlvNP9n3hGEcgPaXyvTljanAKIUkuG9URQ1DeuDup0OR7Ltvoc8yrmH+iMZJbcQGhUJF+WjHLwuk8HSdw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/logger": "3.8.1", + "@docusaurus/mdx-loader": "3.8.1", + "@docusaurus/theme-common": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-common": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "cheerio": "1.0.0-rc.12", "feed": "^4.2.2", "fs-extra": "^11.1.1", "lodash": "^4.17.21", - "reading-time": "^1.5.0", + "schema-dts": "^1.1.2", "srcset": "^4.0.0", "tslib": "^2.6.0", "unist-util-visit": "^5.0.0", @@ -3379,25 +3409,26 @@ } }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.7.0.tgz", - "integrity": "sha512-GXg5V7kC9FZE4FkUZA8oo/NrlRb06UwuICzI6tcbzj0+TVgjq/mpUXXzSgKzMS82YByi4dY2Q808njcBCyy6tQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.1.tgz", + "integrity": "sha512-oByRkSZzeGNQByCMaX+kif5Nl2vmtj2IHQI2fWjCfCootsdKZDPFLonhIp5s3IGJO7PLUfe0POyw0Xh/RrGXJA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/logger": "3.8.1", + "@docusaurus/mdx-loader": "3.8.1", + "@docusaurus/module-type-aliases": "3.8.1", + "@docusaurus/theme-common": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-common": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "@types/react-router-config": "^5.0.7", "combine-promises": "^1.1.0", "fs-extra": "^11.1.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", + "schema-dts": "^1.1.2", "tslib": "^2.6.0", "utility-types": "^3.10.0", "webpack": "^5.88.1" @@ -3411,16 +3442,16 @@ } }, "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.7.0.tgz", - "integrity": "sha512-YJSU3tjIJf032/Aeao8SZjFOrXJbz/FACMveSMjLyMH4itQyZ2XgUIzt4y+1ISvvk5zrW4DABVT2awTCqBkx0Q==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.8.1.tgz", + "integrity": "sha512-a+V6MS2cIu37E/m7nDJn3dcxpvXb6TvgdNI22vJX8iUTp8eoMoPa0VArEbWvCxMY/xdC26WzNv4wZ6y0iIni/w==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/mdx-loader": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "fs-extra": "^11.1.1", "tslib": "^2.6.0", "webpack": "^5.88.1" @@ -3433,17 +3464,33 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, - "node_modules/@docusaurus/plugin-debug": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.7.0.tgz", - "integrity": "sha512-Qgg+IjG/z4svtbCNyTocjIwvNTNEwgRjSXXSJkKVG0oWoH0eX/HAPiu+TS1HBwRPQV+tTYPWLrUypYFepfujZA==", + "node_modules/@docusaurus/plugin-css-cascade-layers": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.8.1.tgz", + "integrity": "sha512-VQ47xRxfNKjHS5ItzaVXpxeTm7/wJLFMOPo1BkmoMG4Cuz4nuI+Hs62+RMk1OqVog68Swz66xVPK8g9XTrBKRw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@docusaurus/plugin-debug": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.8.1.tgz", + "integrity": "sha512-nT3lN7TV5bi5hKMB7FK8gCffFTBSsBsAfV84/v293qAmnHOyg1nr9okEw8AiwcO3bl9vije5nsUvP0aRl2lpaw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", "fs-extra": "^11.1.1", - "react-json-view-lite": "^1.2.0", + "react-json-view-lite": "^2.3.0", "tslib": "^2.6.0" }, "engines": { @@ -3455,14 +3502,14 @@ } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.7.0.tgz", - "integrity": "sha512-otIqiRV/jka6Snjf+AqB360XCeSv7lQC+DKYW+EUZf6XbuE8utz5PeUQ8VuOcD8Bk5zvT1MC4JKcd5zPfDuMWA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.8.1.tgz", + "integrity": "sha512-Hrb/PurOJsmwHAsfMDH6oVpahkEGsx7F8CWMjyP/dw1qjqmdS9rcV1nYCGlM8nOtD3Wk/eaThzUB5TSZsGz+7Q==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "tslib": "^2.6.0" }, "engines": { @@ -3474,14 +3521,14 @@ } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.7.0.tgz", - "integrity": "sha512-M3vrMct1tY65ModbyeDaMoA+fNJTSPe5qmchhAbtqhDD/iALri0g9LrEpIOwNaoLmm6lO88sfBUADQrSRSGSWA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.8.1.tgz", + "integrity": "sha512-tKE8j1cEZCh8KZa4aa80zpSTxsC2/ZYqjx6AAfd8uA8VHZVw79+7OTEP2PoWi0uL5/1Is0LF5Vwxd+1fz5HlKg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "@types/gtag.js": "^0.0.12", "tslib": "^2.6.0" }, @@ -3494,14 +3541,14 @@ } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.7.0.tgz", - "integrity": "sha512-X8U78nb8eiMiPNg3jb9zDIVuuo/rE1LjGDGu+5m5CX4UBZzjMy+klOY2fNya6x8ACyE/L3K2erO1ErheP55W/w==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.8.1.tgz", + "integrity": "sha512-iqe3XKITBquZq+6UAXdb1vI0fPY5iIOitVjPQ581R1ZKpHr0qe+V6gVOrrcOHixPDD/BUKdYwkxFjpNiEN+vBw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "tslib": "^2.6.0" }, "engines": { @@ -3513,17 +3560,17 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.7.0.tgz", - "integrity": "sha512-bTRT9YLZ/8I/wYWKMQke18+PF9MV8Qub34Sku6aw/vlZ/U+kuEuRpQ8bTcNOjaTSfYsWkK4tTwDMHK2p5S86cA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.8.1.tgz", + "integrity": "sha512-+9YV/7VLbGTq8qNkjiugIelmfUEVkTyLe6X8bWq7K5qPvGXAjno27QAfFq63mYfFFbJc7z+pudL63acprbqGzw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/logger": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-common": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "fs-extra": "^11.1.1", "sitemap": "^7.1.1", "tslib": "^2.6.0" @@ -3537,15 +3584,15 @@ } }, "node_modules/@docusaurus/plugin-svgr": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.7.0.tgz", - "integrity": "sha512-HByXIZTbc4GV5VAUkZ2DXtXv1Qdlnpk3IpuImwSnEzCDBkUMYcec5282hPjn6skZqB25M1TYCmWS91UbhBGxQg==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.8.1.tgz", + "integrity": "sha512-rW0LWMDsdlsgowVwqiMb/7tANDodpy1wWPwCcamvhY7OECReN3feoFwLjd/U4tKjNY3encj0AJSTxJA+Fpe+Gw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "@svgr/core": "8.1.0", "@svgr/webpack": "^8.1.0", "tslib": "^2.6.0", @@ -3560,25 +3607,26 @@ } }, "node_modules/@docusaurus/preset-classic": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.7.0.tgz", - "integrity": "sha512-nPHj8AxDLAaQXs+O6+BwILFuhiWbjfQWrdw2tifOClQoNfuXDjfjogee6zfx6NGHWqshR23LrcN115DmkHC91Q==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.8.1.tgz", + "integrity": "sha512-yJSjYNHXD8POMGc2mKQuj3ApPrN+eG0rO1UPgSx7jySpYU+n4WjBikbrA2ue5ad9A7aouEtMWUoiSRXTH/g7KQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/plugin-content-blog": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/plugin-content-pages": "3.7.0", - "@docusaurus/plugin-debug": "3.7.0", - "@docusaurus/plugin-google-analytics": "3.7.0", - "@docusaurus/plugin-google-gtag": "3.7.0", - "@docusaurus/plugin-google-tag-manager": "3.7.0", - "@docusaurus/plugin-sitemap": "3.7.0", - "@docusaurus/plugin-svgr": "3.7.0", - "@docusaurus/theme-classic": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-search-algolia": "3.7.0", - "@docusaurus/types": "3.7.0" + "@docusaurus/core": "3.8.1", + "@docusaurus/plugin-content-blog": "3.8.1", + "@docusaurus/plugin-content-docs": "3.8.1", + "@docusaurus/plugin-content-pages": "3.8.1", + "@docusaurus/plugin-css-cascade-layers": "3.8.1", + "@docusaurus/plugin-debug": "3.8.1", + "@docusaurus/plugin-google-analytics": "3.8.1", + "@docusaurus/plugin-google-gtag": "3.8.1", + "@docusaurus/plugin-google-tag-manager": "3.8.1", + "@docusaurus/plugin-sitemap": "3.8.1", + "@docusaurus/plugin-svgr": "3.8.1", + "@docusaurus/theme-classic": "3.8.1", + "@docusaurus/theme-common": "3.8.1", + "@docusaurus/theme-search-algolia": "3.8.1", + "@docusaurus/types": "3.8.1" }, "engines": { "node": ">=18.0" @@ -3589,31 +3637,31 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.7.0.tgz", - "integrity": "sha512-MnLxG39WcvLCl4eUzHr0gNcpHQfWoGqzADCly54aqCofQX6UozOS9Th4RK3ARbM9m7zIRv3qbhggI53dQtx/hQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.8.1.tgz", + "integrity": "sha512-bqDUCNqXeYypMCsE1VcTXSI1QuO4KXfx8Cvl6rYfY0bhhqN6d2WZlRkyLg/p6pm+DzvanqHOyYlqdPyP0iz+iw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/plugin-content-blog": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/plugin-content-pages": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-translations": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/logger": "3.8.1", + "@docusaurus/mdx-loader": "3.8.1", + "@docusaurus/module-type-aliases": "3.8.1", + "@docusaurus/plugin-content-blog": "3.8.1", + "@docusaurus/plugin-content-docs": "3.8.1", + "@docusaurus/plugin-content-pages": "3.8.1", + "@docusaurus/theme-common": "3.8.1", + "@docusaurus/theme-translations": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-common": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "copy-text-to-clipboard": "^3.2.0", "infima": "0.2.0-alpha.45", "lodash": "^4.17.21", "nprogress": "^0.2.0", - "postcss": "^8.4.26", + "postcss": "^8.5.4", "prism-react-renderer": "^2.3.0", "prismjs": "^1.29.0", "react-router-dom": "^5.3.4", @@ -3630,15 +3678,15 @@ } }, "node_modules/@docusaurus/theme-common": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.7.0.tgz", - "integrity": "sha512-8eJ5X0y+gWDsURZnBfH0WabdNm8XMCXHv8ENy/3Z/oQKwaB/EHt5lP9VsTDTf36lKEp0V6DjzjFyFIB+CetL0A==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.8.1.tgz", + "integrity": "sha512-UswMOyTnPEVRvN5Qzbo+l8k4xrd5fTFu2VPPfD6FcW/6qUtVLmJTQCktbAL3KJ0BVXGm5aJXz/ZrzqFuZERGPw==", "license": "MIT", "dependencies": { - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@docusaurus/mdx-loader": "3.8.1", + "@docusaurus/module-type-aliases": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-common": "3.8.1", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -3658,19 +3706,19 @@ } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.7.0.tgz", - "integrity": "sha512-Al/j5OdzwRU1m3falm+sYy9AaB93S1XF1Lgk9Yc6amp80dNxJVplQdQTR4cYdzkGtuQqbzUA8+kaoYYO0RbK6g==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.8.1.tgz", + "integrity": "sha512-NBFH5rZVQRAQM087aYSRKQ9yGEK9eHd+xOxQjqNpxMiV85OhJDD4ZGz6YJIod26Fbooy54UWVdzNU0TFeUUUzQ==", "license": "MIT", "dependencies": { - "@docsearch/react": "^3.8.1", - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-translations": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docsearch/react": "^3.9.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/logger": "3.8.1", + "@docusaurus/plugin-content-docs": "3.8.1", + "@docusaurus/theme-common": "3.8.1", + "@docusaurus/theme-translations": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "algoliasearch": "^5.17.1", "algoliasearch-helper": "^3.22.6", "clsx": "^2.0.0", @@ -3689,9 +3737,9 @@ } }, "node_modules/@docusaurus/theme-translations": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.7.0.tgz", - "integrity": "sha512-Ewq3bEraWDmienM6eaNK7fx+/lHMtGDHQyd1O+4+3EsDxxUmrzPkV7Ct3nBWTuE0MsoZr3yNwQVKjllzCMuU3g==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.8.1.tgz", + "integrity": "sha512-OTp6eebuMcf2rJt4bqnvuwmm3NVXfzfYejL+u/Y1qwKhZPrjPoKWfk1CbOP5xH5ZOPkiAsx4dHdQBRJszK3z2g==", "license": "MIT", "dependencies": { "fs-extra": "^11.1.1", @@ -3702,16 +3750,16 @@ } }, "node_modules/@docusaurus/tsconfig": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.7.0.tgz", - "integrity": "sha512-vRsyj3yUZCjscgfgcFYjIsTcAru/4h4YH2/XAE8Rs7wWdnng98PgWKvP5ovVc4rmRpRg2WChVW0uOy2xHDvDBQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.8.1.tgz", + "integrity": "sha512-XBWCcqhRHhkhfolnSolNL+N7gj3HVE3CoZVqnVjfsMzCoOsuQw2iCLxVVHtO+rePUUfouVZHURDgmqIySsF66A==", "dev": true, "license": "MIT" }, "node_modules/@docusaurus/types": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz", - "integrity": "sha512-kOmZg5RRqJfH31m+6ZpnwVbkqMJrPOG5t0IOl4i/+3ruXyNfWzZ0lVtVrD0u4ONc/0NOsS9sWYaxxWNkH1LdLQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.8.1.tgz", + "integrity": "sha512-ZPdW5AB+pBjiVrcLuw3dOS6BFlrG0XkS2lDGsj8TizcnREQg3J8cjsgfDviszOk4CweNfwo1AEELJkYaMUuOPg==", "license": "MIT", "dependencies": { "@mdx-js/mdx": "^3.0.0", @@ -3744,15 +3792,16 @@ } }, "node_modules/@docusaurus/utils": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.7.0.tgz", - "integrity": "sha512-e7zcB6TPnVzyUaHMJyLSArKa2AG3h9+4CfvKXKKWNx6hRs+p0a+u7HHTJBgo6KW2m+vqDnuIHK4X+bhmoghAFA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.8.1.tgz", + "integrity": "sha512-P1ml0nvOmEFdmu0smSXOqTS1sxU5tqvnc0dA4MTKV39kye+bhQnjkIKEE18fNOvxjyB86k8esoCIFM3x4RykOQ==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@docusaurus/logger": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils-common": "3.8.1", "escape-string-regexp": "^4.0.0", + "execa": "5.1.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", "github-slugger": "^1.5.0", @@ -3762,9 +3811,9 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "micromatch": "^4.0.5", + "p-queue": "^6.6.2", "prompts": "^2.4.2", "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", "tslib": "^2.6.0", "url-loader": "^4.1.1", "utility-types": "^3.10.0", @@ -3775,12 +3824,12 @@ } }, "node_modules/@docusaurus/utils-common": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.7.0.tgz", - "integrity": "sha512-IZeyIfCfXy0Mevj6bWNg7DG7B8G+S6o6JVpddikZtWyxJguiQ7JYr0SIZ0qWd8pGNuMyVwriWmbWqMnK7Y5PwA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.8.1.tgz", + "integrity": "sha512-zTZiDlvpvoJIrQEEd71c154DkcriBecm4z94OzEE9kz7ikS3J+iSlABhFXM45mZ0eN5pVqqr7cs60+ZlYLewtg==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.7.0", + "@docusaurus/types": "3.8.1", "tslib": "^2.6.0" }, "engines": { @@ -3788,14 +3837,14 @@ } }, "node_modules/@docusaurus/utils-validation": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.7.0.tgz", - "integrity": "sha512-w8eiKk8mRdN+bNfeZqC4nyFoxNyI1/VExMKAzD9tqpJfLLbsa46Wfn5wcKH761g9WkKh36RtFV49iL9lh1DYBA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.8.1.tgz", + "integrity": "sha512-gs5bXIccxzEbyVecvxg6upTwaUbfa0KMmTj7HhHzc016AGyxH2o73k1/aOD0IFrdCsfJNt37MqNI47s2MgRZMA==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@docusaurus/logger": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-common": "3.8.1", "fs-extra": "^11.2.0", "joi": "^17.9.2", "js-yaml": "^4.1.0", @@ -4757,12 +4806,6 @@ "@types/node": "*" } }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, "node_modules/@types/parse5": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", @@ -5437,15 +5480,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/autocomplete.js": { "version": "0.37.1", "resolved": "https://registry.npmjs.org/autocomplete.js/-/autocomplete.js-0.37.1.tgz", @@ -5775,9 +5809,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "funding": [ { "type": "opencollective", @@ -5794,10 +5828,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -5978,9 +6012,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001713", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz", - "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==", + "version": "1.0.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", "funding": [ { "type": "opencollective", @@ -7061,9 +7095,9 @@ } }, "node_modules/cssdb": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.2.5.tgz", - "integrity": "sha512-leAt8/hdTCtzql9ZZi86uYAmCLzVKpJMMdjbvOGVnXFXz/BWFpBmM1MHEHU/RqtPyRYmabVmEW1DtX3YGLuuLA==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.3.1.tgz", + "integrity": "sha512-XnDRQMXucLueX92yDe0LPKupXetWoFOgawr4O4X41l5TltgK2NVbJJVDnnOywDYfW1sTJ28AcXGKOqdRKwCcmQ==", "funding": [ { "type": "opencollective", @@ -7369,28 +7403,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "license": "MIT", - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7463,38 +7475,6 @@ "node": ">= 4.0.0" } }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "license": "MIT", - "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" - }, - "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/detect-port-alt/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/detect-port-alt/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -7936,9 +7916,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz", - "integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==", + "version": "1.5.178", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.178.tgz", + "integrity": "sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -8685,15 +8665,6 @@ "node": ">=0.10.0" } }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8834,134 +8805,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/form-data": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", @@ -9281,44 +9124,6 @@ "node": ">=10" } }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "license": "MIT", - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "license": "MIT", - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -10644,13 +10449,10 @@ } }, "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, "bin": { "image-size": "bin/image-size.js" }, @@ -10994,15 +10796,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -11045,15 +10838,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -14650,6 +14434,15 @@ "node": ">=12.20" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", @@ -14695,6 +14488,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -14708,13 +14517,16 @@ "node": ">=8" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/package-json": { @@ -15012,83 +14824,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -15105,7 +14844,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -15183,9 +14922,9 @@ } }, "node_modules/postcss-color-functional-notation": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.8.tgz", - "integrity": "sha512-S/TpMKVKofNvsxfau/+bw+IA6cSfB6/kmzFj5szUofHOVnFFMB2WwK+Zu07BeMD8T0n+ZnTO5uXiMvAKe2dPkA==", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.10.tgz", + "integrity": "sha512-k9qX+aXHBiLTRrWoCJuUFI6F1iF6QJQUXNVWJVSbqZgj57jDhBlOvD8gNUGl35tgqDivbGLhZeW3Ongz4feuKA==", "funding": [ { "type": "github", @@ -15198,10 +14937,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.8", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -15298,9 +15037,9 @@ } }, "node_modules/postcss-custom-media": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.5.tgz", - "integrity": "sha512-SQHhayVNgDvSAdX9NQ/ygcDQGEY+aSF4b/96z7QUX6mqL5yl/JgG/DywcF6fW9XbnCRE+aVYk+9/nqGuzOPWeQ==", + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", + "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", "funding": [ { "type": "github", @@ -15313,10 +15052,10 @@ ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/media-query-list-parser": "^4.0.2" + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" }, "engines": { "node": ">=18" @@ -15326,9 +15065,9 @@ } }, "node_modules/postcss-custom-properties": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.4.tgz", - "integrity": "sha512-QnW8FCCK6q+4ierwjnmXF9Y9KF8q0JkbgVfvQEMa93x1GT8FvOiUevWCN2YLaOWyByeDX8S6VFbZEeWoAoXs2A==", + "version": "14.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", + "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", "funding": [ { "type": "github", @@ -15341,9 +15080,9 @@ ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, @@ -15355,9 +15094,9 @@ } }, "node_modules/postcss-custom-selectors": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.4.tgz", - "integrity": "sha512-ASOXqNvDCE0dAJ/5qixxPeL1aOVGHGW2JwSy7HyjWNbnWTQCl+fDc968HY1jCmZI0+BaYT5CxsOiUhavpG/7eg==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", + "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", "funding": [ { "type": "github", @@ -15370,9 +15109,9 @@ ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", "postcss-selector-parser": "^7.0.0" }, "engines": { @@ -15497,9 +15236,9 @@ } }, "node_modules/postcss-double-position-gradients": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.0.tgz", - "integrity": "sha512-JkIGah3RVbdSEIrcobqj4Gzq0h53GG4uqDPsho88SgY84WnpkTpI0k50MFK/sX7XqVisZ6OqUfFnoUO6m1WWdg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.2.tgz", + "integrity": "sha512-7qTqnL7nfLRyJK/AHSVrrXOuvDDzettC+wGoienURV8v2svNbu6zJC52ruZtHaO6mfcagFmuTGFdzRsJKB3k5Q==", "funding": [ { "type": "github", @@ -15512,7 +15251,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, @@ -15693,9 +15432,9 @@ } }, "node_modules/postcss-lab-function": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.8.tgz", - "integrity": "sha512-plV21I86Hg9q8omNz13G9fhPtLopIWH06bt/Cb5cs1XnaGU2kUtEitvVd4vtQb/VqCdNUHK5swKn3QFmMRbpDg==", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.10.tgz", + "integrity": "sha512-tqs6TCEv9tC1Riq6fOzHuHcZyhg4k3gIAMB8GGY/zA1ssGdm6puHMVE7t75aOSoFg7UD2wyrFFhbldiCMyyFTQ==", "funding": [ { "type": "github", @@ -15708,10 +15447,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.8", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -16040,9 +15779,9 @@ } }, "node_modules/postcss-nesting": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.1.tgz", - "integrity": "sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", "funding": [ { "type": "github", @@ -16055,7 +15794,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/selector-resolve-nested": "^3.0.0", + "@csstools/selector-resolve-nested": "^3.1.0", "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0" }, @@ -16067,9 +15806,9 @@ } }, "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz", - "integrity": "sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", "funding": [ { "type": "github", @@ -16354,9 +16093,9 @@ } }, "node_modules/postcss-preset-env": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.1.5.tgz", - "integrity": "sha512-LQybafF/K7H+6fAs4SIkgzkSCixJy0/h0gubDIAP3Ihz+IQBRwsjyvBnAZ3JUHD+A/ITaxVRPDxn//a3Qy4pDw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.2.4.tgz", + "integrity": "sha512-q+lXgqmTMdB0Ty+EQ31SuodhdfZetUlwCA/F0zRcd/XdxjzI+Rl2JhZNz5US2n/7t9ePsvuhCnEN4Bmu86zXlA==", "funding": [ { "type": "github", @@ -16369,62 +16108,63 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-cascade-layers": "^5.0.1", - "@csstools/postcss-color-function": "^4.0.8", - "@csstools/postcss-color-mix-function": "^3.0.8", - "@csstools/postcss-content-alt-text": "^2.0.4", - "@csstools/postcss-exponential-functions": "^2.0.7", + "@csstools/postcss-cascade-layers": "^5.0.2", + "@csstools/postcss-color-function": "^4.0.10", + "@csstools/postcss-color-mix-function": "^3.0.10", + "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.0", + "@csstools/postcss-content-alt-text": "^2.0.6", + "@csstools/postcss-exponential-functions": "^2.0.9", "@csstools/postcss-font-format-keywords": "^4.0.0", - "@csstools/postcss-gamut-mapping": "^2.0.8", - "@csstools/postcss-gradients-interpolation-method": "^5.0.8", - "@csstools/postcss-hwb-function": "^4.0.8", - "@csstools/postcss-ic-unit": "^4.0.0", + "@csstools/postcss-gamut-mapping": "^2.0.10", + "@csstools/postcss-gradients-interpolation-method": "^5.0.10", + "@csstools/postcss-hwb-function": "^4.0.10", + "@csstools/postcss-ic-unit": "^4.0.2", "@csstools/postcss-initial": "^2.0.1", - "@csstools/postcss-is-pseudo-class": "^5.0.1", - "@csstools/postcss-light-dark-function": "^2.0.7", + "@csstools/postcss-is-pseudo-class": "^5.0.3", + "@csstools/postcss-light-dark-function": "^2.0.9", "@csstools/postcss-logical-float-and-clear": "^3.0.0", "@csstools/postcss-logical-overflow": "^2.0.0", "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", "@csstools/postcss-logical-resize": "^3.0.0", - "@csstools/postcss-logical-viewport-units": "^3.0.3", - "@csstools/postcss-media-minmax": "^2.0.7", - "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.4", + "@csstools/postcss-logical-viewport-units": "^3.0.4", + "@csstools/postcss-media-minmax": "^2.0.9", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", "@csstools/postcss-nested-calc": "^4.0.0", "@csstools/postcss-normalize-display-values": "^4.0.0", - "@csstools/postcss-oklab-function": "^4.0.8", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", - "@csstools/postcss-random-function": "^1.0.3", - "@csstools/postcss-relative-color-syntax": "^3.0.8", + "@csstools/postcss-oklab-function": "^4.0.10", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-random-function": "^2.0.1", + "@csstools/postcss-relative-color-syntax": "^3.0.10", "@csstools/postcss-scope-pseudo-class": "^4.0.1", - "@csstools/postcss-sign-functions": "^1.1.2", - "@csstools/postcss-stepped-value-functions": "^4.0.7", + "@csstools/postcss-sign-functions": "^1.1.4", + "@csstools/postcss-stepped-value-functions": "^4.0.9", "@csstools/postcss-text-decoration-shorthand": "^4.0.2", - "@csstools/postcss-trigonometric-functions": "^4.0.7", + "@csstools/postcss-trigonometric-functions": "^4.0.9", "@csstools/postcss-unset-value": "^4.0.0", - "autoprefixer": "^10.4.19", - "browserslist": "^4.24.4", + "autoprefixer": "^10.4.21", + "browserslist": "^4.25.0", "css-blank-pseudo": "^7.0.1", "css-has-pseudo": "^7.0.2", "css-prefers-color-scheme": "^10.0.0", - "cssdb": "^8.2.3", + "cssdb": "^8.3.0", "postcss-attribute-case-insensitive": "^7.0.1", "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^7.0.8", + "postcss-color-functional-notation": "^7.0.10", "postcss-color-hex-alpha": "^10.0.0", "postcss-color-rebeccapurple": "^10.0.0", - "postcss-custom-media": "^11.0.5", - "postcss-custom-properties": "^14.0.4", - "postcss-custom-selectors": "^8.0.4", + "postcss-custom-media": "^11.0.6", + "postcss-custom-properties": "^14.0.6", + "postcss-custom-selectors": "^8.0.5", "postcss-dir-pseudo-class": "^9.0.1", - "postcss-double-position-gradients": "^6.0.0", + "postcss-double-position-gradients": "^6.0.2", "postcss-focus-visible": "^10.0.1", "postcss-focus-within": "^9.0.1", "postcss-font-variant": "^5.0.0", "postcss-gap-properties": "^6.0.0", "postcss-image-set-function": "^7.0.0", - "postcss-lab-function": "^7.0.8", + "postcss-lab-function": "^7.0.10", "postcss-logical": "^8.1.0", - "postcss-nesting": "^13.0.1", + "postcss-nesting": "^13.0.2", "postcss-opacity-percentage": "^3.0.0", "postcss-overflow-shorthand": "^6.0.0", "postcss-page-break": "^3.0.4", @@ -16719,9 +16459,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -16894,15 +16634,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -17094,132 +16825,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dev-utils/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -17233,12 +16838,6 @@ "react": "^18.3.1" } }, - "node_modules/react-error-overlay": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz", - "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==", - "license": "MIT" - }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -17270,15 +16869,15 @@ "license": "MIT" }, "node_modules/react-json-view-lite": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-1.5.0.tgz", - "integrity": "sha512-nWqA1E4jKPklL2jvHWs6s+7Na0qNgw9HCP6xehdQJeg6nPBTFZgGwyko9Q0oj+jQWKTTVRS30u0toM5wiuL3iw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.4.1.tgz", + "integrity": "sha512-fwFYknRIBxjbFm0kBDrzgBy1xa5tDg2LyXXBepC5f1b+MY3BUClMCsvanMPn089JbV1Eg3nZcrp0VCuH43aXnA==", "license": "MIT", "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/react-loadable": { @@ -17433,12 +17032,6 @@ "node": ">=8.10.0" } }, - "node_modules/reading-time": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", - "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==", - "license": "MIT" - }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -17514,18 +17107,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -18208,6 +17789,12 @@ "loose-envify": "^1.1.0" } }, + "node_modules/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "license": "Apache-2.0" + }, "node_modules/schema-utils": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", @@ -19472,12 +19059,6 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "license": "MIT" - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -19517,6 +19098,15 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -19686,6 +19276,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/docs/package.json b/docs/package.json index e13e85ecb3..97072d8eb5 100644 --- a/docs/package.json +++ b/docs/package.json @@ -16,8 +16,9 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@docusaurus/core": "~3.7.0", - "@docusaurus/preset-classic": "~3.7.0", + "@docusaurus/core": "~3.8.0", + "@docusaurus/preset-classic": "~3.8.0", + "@docusaurus/theme-common": "~3.8.0", "@mdi/js": "^7.3.67", "@mdi/react": "^1.6.1", "@mdx-js/react": "^3.0.0", @@ -26,6 +27,7 @@ "clsx": "^2.0.0", "docusaurus-lunr-search": "^3.3.2", "docusaurus-preset-openapi": "^0.7.5", + "lunr": "^2.3.9", "postcss": "^8.4.25", "prism-react-renderer": "^2.3.1", "raw-loader": "^4.0.2", @@ -35,7 +37,7 @@ "url": "^0.11.0" }, "devDependencies": { - "@docusaurus/module-type-aliases": "~3.7.0", + "@docusaurus/module-type-aliases": "~3.8.0", "@docusaurus/tsconfig": "^3.7.0", "@docusaurus/types": "^3.7.0", "prettier": "^3.2.4", @@ -57,6 +59,6 @@ "node": ">=20" }, "volta": { - "node": "22.16.0" + "node": "22.17.1" } } diff --git a/docs/src/components/community-guides.tsx b/docs/src/components/community-guides.tsx index f5331a9163..49ba7a8a08 100644 --- a/docs/src/components/community-guides.tsx +++ b/docs/src/components/community-guides.tsx @@ -58,6 +58,12 @@ const guides: CommunityGuidesProps[] = [ description: 'Access Immich with an end-to-end encrypted connection.', url: 'https://meshnet.nordvpn.com/how-to/remote-files-media-access/immich-remote-access', }, + { + title: 'Trust Self Signed Certificates with Immich - OAuth Setup', + description: + 'Set up Certificate Authority trust with Immich, and your private OAuth2/OpenID service, while using a private CA for HTTPS commication.', + url: 'https://github.com/immich-app/immich/discussions/18614', + }, ]; function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element { diff --git a/docs/src/pages/roadmap.tsx b/docs/src/pages/roadmap.tsx index 1258000052..e002c4d032 100644 --- a/docs/src/pages/roadmap.tsx +++ b/docs/src/pages/roadmap.tsx @@ -85,6 +85,7 @@ import React from 'react'; import { Item, Timeline } from '../components/timeline'; const releases = { + 'v1.135.0': new Date(2025, 5, 18), 'v1.133.0': new Date(2025, 4, 21), 'v1.130.0': new Date(2025, 2, 25), 'v1.127.0': new Date(2025, 1, 26), @@ -196,14 +197,6 @@ const roadmap: Item[] = [ description: 'Automate tasks with workflows', getDateLabel: () => 'Planned for 2025', }, - { - done: false, - icon: mdiTableKey, - iconColor: 'gray', - title: 'Fine grained access controls', - description: 'Granular access controls for users and api keys', - getDateLabel: () => 'Planned for 2025', - }, { done: false, icon: mdiImageEdit, @@ -239,12 +232,26 @@ const roadmap: Item[] = [ ]; const milestones: Item[] = [ + { + icon: mdiStar, + iconColor: 'gold', + title: '70,000 Stars', + description: 'Reached 70K Stars on GitHub!', + getDateLabel: withLanguage(new Date(2025, 6, 9)), + }, + withRelease({ + icon: mdiTableKey, + iconColor: 'gray', + title: 'Fine grained access controls', + description: 'Granular access controls for api keys', + release: 'v1.135.0', + }), withRelease({ icon: mdiCast, iconColor: 'aqua', - title: 'Google Cast (web)', + title: 'Google Cast (web and mobile)', description: 'Cast assets to Google Cast/Chromecast compatible devices', - release: 'v1.133.0', + release: 'v1.135.0', }), withRelease({ icon: mdiLockOutline, diff --git a/docs/static/_redirects b/docs/static/_redirects index 6683c78077..7b01d1e3bb 100644 --- a/docs/static/_redirects +++ b/docs/static/_redirects @@ -1,4 +1,5 @@ -/docs /docs/overview/introduction 307 +/docs /docs/overview/welcome 307 +/docs/ /docs/overview/welcome 307 /docs/mobile-app-beta-program /docs/features/mobile-app 307 /docs/contribution-guidelines /docs/overview/support-the-project#contributing 307 /docs/install /docs/install/docker-compose 307 @@ -30,4 +31,4 @@ /docs/guides/api-album-sync /docs/community-projects 307 /docs/guides/remove-offline-files /docs/community-projects 307 /milestones /roadmap 307 -/docs/overview/introduction /docs/overview/welcome 307 \ No newline at end of file +/docs/overview/introduction /docs/overview/welcome 307 diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index adbd9aa717..6018e26011 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.136.0", + "url": "https://v1.136.0.archive.immich.app" + }, { "label": "v1.135.3", "url": "https://v1.135.3.archive.immich.app" diff --git a/e2e/.nvmrc b/e2e/.nvmrc index 5b540673a8..7377d130ed 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -22.16.0 +22.17.1 diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 5188a2d017..ed57f423bd 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -3,7 +3,6 @@ name: immich-e2e services: immich-server: container_name: immich-e2e-server - command: ['./start.sh'] image: immich-server:latest build: context: ../ @@ -23,6 +22,7 @@ services: - IMMICH_ENV=testing - IMMICH_PORT=2285 - IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true + - IMMICH_MEDIA_LOCATION=/data volumes: - ./test-assets:/test-assets extra_hosts: @@ -36,10 +36,10 @@ services: - 2285:2285 redis: - image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa + image: redis:6.2-alpine@sha256:7fe72c486b910f6b1a9769c937dad5d63648ddee82e056f47417542dd40825bb database: - image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:9c704fb49ce27549df00f1b096cc93f8b0c959ef087507704d74954808f78a82 + image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:3aef84a0a4fabbda17ef115c3019ba0c914ec73e9f6e59203674322d858b8eea command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf environment: POSTGRES_PASSWORD: postgres diff --git a/e2e/package-lock.json b/e2e/package-lock.json index e72c7ebed9..5da041b37b 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.135.3", + "version": "1.136.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.135.3", + "version": "1.136.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -14,8 +14,9 @@ "@immich/cli": "file:../cli", "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", + "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", - "@types/node": "^22.15.31", + "@types/node": "^22.16.4", "@types/oidc-provider": "^9.0.0", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", @@ -34,6 +35,7 @@ "pngjs": "^7.0.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", + "sharp": "^0.34.0", "socket.io-client": "^4.7.4", "supertest": "^7.0.0", "typescript": "^5.3.3", @@ -44,7 +46,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.72", + "version": "2.2.73", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -55,7 +57,7 @@ "micromatch": "^4.0.8" }, "bin": { - "immich": "dist/index.js" + "immich": "bin/immich" }, "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -66,7 +68,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.15.31", + "@types/node": "^22.16.4", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -81,7 +83,7 @@ "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "typescript-eslint": "^8.28.0", - "vite": "^6.0.0", + "vite": "^7.0.0", "vite-tsconfig-paths": "^5.0.0", "vitest": "^3.0.0", "vitest-fetch-mock": "^0.4.0", @@ -93,14 +95,14 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.135.3", + "version": "1.136.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.15.31", + "@types/node": "^22.16.4", "typescript": "^5.3.3" } }, @@ -178,6 +180,17 @@ "node": ">=18" } }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", @@ -646,9 +659,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -661,9 +674,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -721,9 +734,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { @@ -823,6 +836,446 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@immich/cli": { "resolved": "../cli", "link": true @@ -926,12 +1379,13 @@ } }, "node_modules/@koa/router": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.0.tgz", - "integrity": "sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.1.tgz", + "integrity": "sha512-JQEuMANYRVHs7lm7KY9PCIjkgJk73h4m4J+g2mkw2Vo1ugPZ17UJVqEH8F+HeAdjKz5do1OaLe7ArDz+z308gw==", "dev": true, "license": "MIT", "dependencies": { + "debug": "^4.4.1", "http-errors": "^2.0.0", "koa-compose": "^4.1.0", "path-to-regexp": "^6.3.0" @@ -1080,13 +1534,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", - "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", + "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.52.0" + "playwright": "1.54.1" }, "bin": { "playwright": "cli.js" @@ -1403,6 +1857,16 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -1440,6 +1904,13 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -1549,9 +2020,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.32", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz", - "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==", + "version": "22.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", + "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1559,9 +2030,9 @@ } }, "node_modules/@types/oidc-provider": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-9.1.0.tgz", - "integrity": "sha512-UoC3ZQur+TtVL5hiUN8LoCbXocS2WI2eAPBtZtv1Y5F3vW0QTBawFAgDoctPqCQF73kah/Nzb5Gd3m5GtxFxiA==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-9.1.1.tgz", + "integrity": "sha512-sG4UcE4AbUwAsEpyrcyoqZ383wJiQObZU+gTa1Iv288+l09HwSr88hBZE2IBLlXS+RKmLId0i4B430PBFO/XRA==", "dev": true, "license": "MIT", "dependencies": { @@ -1571,15 +2042,15 @@ } }, "node_modules/@types/pg": { - "version": "8.15.2", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.2.tgz", - "integrity": "sha512-+BKxo5mM6+/A1soSHBI7ufUglqYXntChLDyTbvcAn1Lawi9J7J9Ok3jt6w7I0+T/UDJ4CyhHk66+GZbwmkYxSg==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz", + "integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "pg-protocol": "*", - "pg-types": "^4.0.1" + "pg-types": "^2.2.0" } }, "node_modules/@types/pngjs": { @@ -1654,17 +2125,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", + "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/type-utils": "8.37.0", + "@typescript-eslint/utils": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1678,15 +2149,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.37.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -1694,16 +2165,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", + "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4" }, "engines": { @@ -1718,15 +2189,37 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", + "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/tsconfig-utils": "^8.37.0", + "@typescript-eslint/types": "^8.37.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", + "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1736,15 +2229,33 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", + "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", + "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1761,9 +2272,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz", + "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==", "dev": true, "license": "MIT", "engines": { @@ -1775,14 +2286,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", + "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.37.0", + "@typescript-eslint/tsconfig-utils": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1802,9 +2315,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1828,16 +2341,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz", + "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1852,14 +2365,14 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", + "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.37.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1870,15 +2383,16 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz", - "integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "debug": "^4.4.0", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", @@ -1893,8 +2407,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.1.4", - "vitest": "3.1.4" + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1903,14 +2417,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", - "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.4", - "@vitest/utils": "3.1.4", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -1919,13 +2434,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", - "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.4", + "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1934,7 +2449,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1946,9 +2461,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", - "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -1959,27 +2474,28 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", - "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.4", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", - "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.4", + "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -1988,27 +2504,27 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", - "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", - "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.4", - "loupe": "^3.1.3", + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { @@ -2037,9 +2553,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -2164,6 +2680,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", + "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2447,6 +2975,20 @@ "node": ">=0.8.0" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2467,6 +3009,17 @@ "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -2668,9 +3221,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2911,19 +3464,19 @@ } }, "node_modules/eslint": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", - "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.27.0", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2935,9 +3488,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2988,14 +3541,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz", - "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", + "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.0" + "synckit": "^0.11.7" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3081,9 +3634,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3098,9 +3651,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3110,16 +3663,29 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3692,9 +4258,9 @@ } }, "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -3964,6 +4530,13 @@ "dev": true, "license": "ISC" }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-builtin-module": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", @@ -4110,6 +4683,13 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4256,9 +4836,9 @@ "license": "MIT" }, "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", "dev": true, "license": "MIT" }, @@ -4270,9 +4850,9 @@ "license": "ISC" }, "node_modules/luxon": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", - "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", "dev": true, "license": "MIT", "engines": { @@ -4611,25 +5191,18 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, - "license": "MIT" - }, "node_modules/oidc-provider": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-9.1.3.tgz", - "integrity": "sha512-DaiiCllAr+y33M2HzTRRpV7/jmcZBSfIXaDv0eHURIV85N0O+QUqtjVtPY+viCwHKBRGWwfShWWFKddPUxpAWw==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-9.4.0.tgz", + "integrity": "sha512-1mEUejJq7cQV/b6cw2nitqOyIlOJTfQ6RNwGFcA7/Pp+vKIWBn8p48ylFtogP3Hbvrkf9s9W5HUeFe+v1KpcEQ==", "dev": true, "license": "MIT", "dependencies": { "@koa/cors": "^5.0.0", - "@koa/router": "^13.1.0", + "@koa/router": "^13.1.1", "debug": "^4.4.1", "eta": "^3.5.0", - "jose": "^6.0.11", + "jose": "^6.0.12", "jsesc": "^3.1.0", "koa": "^3.0.0", "nanoid": "^5.1.5", @@ -4642,9 +5215,9 @@ } }, "node_modules/oidc-provider/node_modules/jose": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", - "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.12.tgz", + "integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==", "dev": true, "license": "MIT", "funding": { @@ -4836,23 +5409,23 @@ } }, "node_modules/pg": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", - "integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==", + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "dev": true, "license": "MIT", "dependencies": { - "pg-connection-string": "^2.9.0", - "pg-pool": "^3.10.0", - "pg-protocol": "^1.10.0", + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "engines": { - "node": ">= 8.0.0" + "node": ">= 16.0.0" }, "optionalDependencies": { - "pg-cloudflare": "^1.2.5" + "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" @@ -4864,17 +5437,17 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz", - "integrity": "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", "dev": true, "license": "MIT", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.0.tgz", - "integrity": "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", "dev": true, "license": "MIT" }, @@ -4888,20 +5461,10 @@ "node": ">=4.0.0" } }, - "node_modules/pg-numeric": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", - "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=4" - } - }, "node_modules/pg-pool": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.0.tgz", - "integrity": "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4909,32 +5472,13 @@ } }, "node_modules/pg-protocol": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.0.tgz", - "integrity": "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", "dev": true, "license": "MIT" }, "node_modules/pg-types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", - "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", - "dev": true, - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "pg-numeric": "1.0.2", - "postgres-array": "~3.0.1", - "postgres-bytea": "~3.0.0", - "postgres-date": "~2.1.0", - "postgres-interval": "^3.0.0", - "postgres-range": "^1.1.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pg/node_modules/pg-types": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", @@ -4951,49 +5495,6 @@ "node": ">=4" } }, - "node_modules/pg/node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pg/node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pg/node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pg/node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pgpass": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", @@ -5025,13 +5526,13 @@ } }, "node_modules/playwright": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", - "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", + "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.52.0" + "playwright-core": "1.54.1" }, "bin": { "playwright": "cli.js" @@ -5044,9 +5545,9 @@ } }, "node_modules/playwright-core": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", - "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", + "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5125,55 +5626,48 @@ } }, "node_modules/postgres-array": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", - "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=4" } }, "node_modules/postgres-bytea": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", - "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", "dev": true, "license": "MIT", - "dependencies": { - "obuf": "~1.1.2" - }, "engines": { - "node": ">= 6" + "node": ">=0.10.0" } }, "node_modules/postgres-date": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", - "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, "node_modules/postgres-interval": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", - "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", "dev": true, "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/postgres-range": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", - "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", - "dev": true, - "license": "MIT" - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5185,9 +5679,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -5510,9 +6004,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -5536,6 +6030,49 @@ "dev": true, "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5655,6 +6192,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/socket.io-client": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", @@ -5908,10 +6455,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/superagent": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz", - "integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.2.tgz", + "integrity": "sha512-vWMq11OwWCC84pQaFPzF/VO3BrjkCeewuvJgt1jfV0499Z1QSAWN4EqfMM5WlFDDX9/oP8JjlDKpblrmEoyu4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5930,14 +6490,14 @@ } }, "node_modules/supertest": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz", - "integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.3.tgz", + "integrity": "sha512-ORY0gPa6ojmg/C74P/bDoS21WL6FMXq5I8mawkEz30/zkwdu0gOeqstFy316vHG6OKxqQ+IbGneRemHI8WraEw==", "dev": true, "license": "MIT", "dependencies": { "methods": "^1.1.2", - "superagent": "^10.2.1" + "superagent": "^10.2.2" }, "engines": { "node": ">=14.18.0" @@ -5957,14 +6517,13 @@ } }, "node_modules/synckit": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", - "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.3", - "tslib": "^2.8.1" + "@pkgr/core": "^0.2.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -6102,9 +6661,9 @@ } }, "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -6122,9 +6681,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, "license": "MIT", "engines": { @@ -6179,7 +6738,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "optional": true }, "node_modules/tsscmp": { "version": "1.0.6", @@ -6257,15 +6817,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz", + "integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.37.0", + "@typescript-eslint/parser": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6445,17 +7006,17 @@ } }, "node_modules/vite-node": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", - "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.4.0", + "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -6511,32 +7072,34 @@ } }, "node_modules/vitest": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", - "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.4", - "@vitest/mocker": "3.1.4", - "@vitest/pretty-format": "^3.1.4", - "@vitest/runner": "3.1.4", - "@vitest/snapshot": "3.1.4", - "@vitest/spy": "3.1.4", - "@vitest/utils": "3.1.4", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", - "debug": "^4.4.0", + "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", + "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.4", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -6552,8 +7115,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.4", - "@vitest/ui": "3.1.4", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, @@ -6581,6 +7144,19 @@ } } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/e2e/package.json b/e2e/package.json index b87f7d2810..580cf4982c 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.135.3", + "version": "1.136.0", "description": "", "main": "index.js", "type": "module", @@ -24,8 +24,9 @@ "@immich/cli": "file:../cli", "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", + "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", - "@types/node": "^22.15.31", + "@types/node": "^22.16.4", "@types/oidc-provider": "^9.0.0", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", @@ -44,6 +45,7 @@ "pngjs": "^7.0.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", + "sharp": "^0.34.0", "socket.io-client": "^4.7.4", "supertest": "^7.0.0", "typescript": "^5.3.3", @@ -52,6 +54,6 @@ "vitest": "^3.0.0" }, "volta": { - "node": "22.16.0" + "node": "22.17.1" } } diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts index 70c32313f1..9ce9b4b916 100644 --- a/e2e/src/api/specs/activity.e2e-spec.ts +++ b/e2e/src/api/specs/activity.e2e-spec.ts @@ -7,6 +7,7 @@ import { ReactionType, createActivity as create, createAlbum, + removeAssetFromAlbum, } from '@immich/sdk'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; @@ -342,5 +343,36 @@ describe('/activities', () => { expect(status).toBe(204); }); + + it('should return empty list when asset is removed', async () => { + const album3 = await createAlbum( + { + createAlbumDto: { + albumName: 'Album 3', + assetIds: [asset.id], + }, + }, + { headers: asBearerAuth(admin.accessToken) }, + ); + + await createActivity({ albumId: album3.id, assetId: asset.id, type: ReactionType.Like }); + + await removeAssetFromAlbum( + { + id: album3.id, + bulkIdsDto: { + ids: [asset.id], + }, + }, + { headers: asBearerAuth(admin.accessToken) }, + ); + + const { status, body } = await request(app) + .get('/activities') + .query({ albumId: album.id }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toEqual(200); + expect(body).toEqual([]); + }); }); }); diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index eedf70dc58..af9b17cc13 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -470,7 +470,7 @@ describe('/albums', () => { .send({ ids: [asset.id] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest('Not found or no album.addAsset access')); + expect(body).toEqual(errorDto.badRequest('Not found or no albumAsset.create access')); }); it('should add duplicate assets only once', async () => { @@ -599,7 +599,7 @@ describe('/albums', () => { .send({ ids: [user1Asset1.id] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest('Not found or no album.removeAsset access')); + expect(body).toEqual(errorDto.badRequest('Not found or no albumAsset.delete access')); }); it('should remove duplicate assets only once', async () => { diff --git a/e2e/src/api/specs/api-key.e2e-spec.ts b/e2e/src/api/specs/api-key.e2e-spec.ts index ad03571869..28d134a664 100644 --- a/e2e/src/api/specs/api-key.e2e-spec.ts +++ b/e2e/src/api/specs/api-key.e2e-spec.ts @@ -20,7 +20,7 @@ describe('/api-keys', () => { }); beforeEach(async () => { - await utils.resetDatabase(['api_keys']); + await utils.resetDatabase(['api_key']); }); describe('POST /api-keys', () => { diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 4673db5426..c1e9f9dfb8 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -15,6 +15,7 @@ import { DateTime } from 'luxon'; import { randomBytes } from 'node:crypto'; import { readFile, writeFile } from 'node:fs/promises'; import { basename, join } from 'node:path'; +import sharp from 'sharp'; import { Socket } from 'socket.io-client'; import { createUserDto, uuidDto } from 'src/fixtures'; import { makeRandomImage } from 'src/generators'; @@ -40,6 +41,40 @@ const today = DateTime.fromObject({ }) as DateTime; const yesterday = today.minus({ days: 1 }); +const createTestImageWithExif = async (filename: string, exifData: Record) => { + // Generate unique color to ensure different checksums for each image + const r = Math.floor(Math.random() * 256); + const g = Math.floor(Math.random() * 256); + const b = Math.floor(Math.random() * 256); + + // Create a 100x100 solid color JPEG using Sharp + const imageBytes = await sharp({ + create: { + width: 100, + height: 100, + channels: 3, + background: { r, g, b }, + }, + }) + .jpeg({ quality: 90 }) + .toBuffer(); + + // Add random suffix to filename to avoid collisions + const uniqueFilename = filename.replace('.jpg', `-${randomBytes(4).toString('hex')}.jpg`); + const filepath = join(tempDir, uniqueFilename); + await writeFile(filepath, imageBytes); + + // Filter out undefined values before writing EXIF + const cleanExifData = Object.fromEntries(Object.entries(exifData).filter(([, value]) => value !== undefined)); + + await exiftool.write(filepath, cleanExifData); + + // Re-read the image bytes after EXIF has been written + const finalImageBytes = await readFile(filepath); + + return { filepath, imageBytes: finalImageBytes, filename: uniqueFilename }; +}; + describe('/asset', () => { let admin: LoginResponseDto; let websocket: Socket; @@ -1190,6 +1225,411 @@ describe('/asset', () => { }); }); + describe('EXIF metadata extraction', () => { + describe('Additional date tag extraction', () => { + describe('Date-time vs time-only tag handling', () => { + it('should fall back to file timestamps when only time-only tags are available', async () => { + const { imageBytes, filename } = await createTestImageWithExif('time-only-fallback.jpg', { + TimeCreated: '2023:11:15 14:30:00', // Time-only tag, should not be used for dateTimeOriginal + // Exclude all date-time tags to force fallback to file timestamps + SubSecDateTimeOriginal: undefined, + DateTimeOriginal: undefined, + SubSecCreateDate: undefined, + SubSecMediaCreateDate: undefined, + CreateDate: undefined, + MediaCreateDate: undefined, + CreationDate: undefined, + DateTimeCreated: undefined, + GPSDateTime: undefined, + DateTimeUTC: undefined, + SonyDateTime2: undefined, + GPSDateStamp: undefined, + }); + + const oldDate = new Date('2020-01-01T00:00:00.000Z'); + const asset = await utils.createAsset(admin.accessToken, { + assetData: { + filename, + bytes: imageBytes, + }, + fileCreatedAt: oldDate.toISOString(), + fileModifiedAt: oldDate.toISOString(), + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); + + const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) }); + + expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined(); + // Should fall back to file timestamps, which we set to 2020-01-01 + expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe( + new Date('2020-01-01T00:00:00.000Z').getTime(), + ); + }); + + it('should prefer DateTimeOriginal over time-only tags', async () => { + const { imageBytes, filename } = await createTestImageWithExif('datetime-over-time.jpg', { + DateTimeOriginal: '2023:10:10 10:00:00', // Should be preferred + TimeCreated: '2023:11:15 14:30:00', // Should be ignored (time-only) + }); + + const asset = await utils.createAsset(admin.accessToken, { + assetData: { + filename, + bytes: imageBytes, + }, + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); + + const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) }); + + expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined(); + // Should use DateTimeOriginal, not TimeCreated + expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe( + new Date('2023-10-10T10:00:00.000Z').getTime(), + ); + }); + }); + + describe('GPSDateTime tag extraction', () => { + it('should extract GPSDateTime with GPS coordinates', async () => { + const { imageBytes, filename } = await createTestImageWithExif('gps-datetime.jpg', { + GPSDateTime: '2023:11:15 12:30:00Z', + GPSLatitude: 37.7749, + GPSLongitude: -122.4194, + // Exclude other date tags + SubSecDateTimeOriginal: undefined, + DateTimeOriginal: undefined, + SubSecCreateDate: undefined, + SubSecMediaCreateDate: undefined, + CreateDate: undefined, + MediaCreateDate: undefined, + CreationDate: undefined, + DateTimeCreated: undefined, + TimeCreated: undefined, + }); + + const asset = await utils.createAsset(admin.accessToken, { + assetData: { + filename, + bytes: imageBytes, + }, + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); + + const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) }); + + expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined(); + expect(assetInfo.exifInfo?.latitude).toBeCloseTo(37.7749, 4); + expect(assetInfo.exifInfo?.longitude).toBeCloseTo(-122.4194, 4); + expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe( + new Date('2023-11-15T12:30:00.000Z').getTime(), + ); + }); + }); + + describe('CreateDate tag extraction', () => { + it('should extract CreateDate when available', async () => { + const { imageBytes, filename } = await createTestImageWithExif('create-date.jpg', { + CreateDate: '2023:11:15 10:30:00', + // Exclude other higher priority date tags + SubSecDateTimeOriginal: undefined, + DateTimeOriginal: undefined, + SubSecCreateDate: undefined, + SubSecMediaCreateDate: undefined, + MediaCreateDate: undefined, + CreationDate: undefined, + DateTimeCreated: undefined, + TimeCreated: undefined, + GPSDateTime: undefined, + }); + + const asset = await utils.createAsset(admin.accessToken, { + assetData: { + filename, + bytes: imageBytes, + }, + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); + + const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) }); + + expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined(); + expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe( + new Date('2023-11-15T10:30:00.000Z').getTime(), + ); + }); + }); + + describe('GPSDateStamp tag extraction', () => { + it('should fall back to file timestamps when only date-only tags are available', async () => { + const { imageBytes, filename } = await createTestImageWithExif('gps-datestamp.jpg', { + GPSDateStamp: '2023:11:15', // Date-only tag, should not be used for dateTimeOriginal + // Note: NOT including GPSTimeStamp to avoid automatic GPSDateTime creation + GPSLatitude: 51.5074, + GPSLongitude: -0.1278, + // Explicitly exclude all testable date-time tags to force fallback to file timestamps + DateTimeOriginal: undefined, + CreateDate: undefined, + CreationDate: undefined, + GPSDateTime: undefined, + }); + + const oldDate = new Date('2020-01-01T00:00:00.000Z'); + const asset = await utils.createAsset(admin.accessToken, { + assetData: { + filename, + bytes: imageBytes, + }, + fileCreatedAt: oldDate.toISOString(), + fileModifiedAt: oldDate.toISOString(), + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); + + const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) }); + + expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined(); + expect(assetInfo.exifInfo?.latitude).toBeCloseTo(51.5074, 4); + expect(assetInfo.exifInfo?.longitude).toBeCloseTo(-0.1278, 4); + // Should fall back to file timestamps, which we set to 2020-01-01 + expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe( + new Date('2020-01-01T00:00:00.000Z').getTime(), + ); + }); + }); + + /* + * NOTE: The following EXIF date tags are NOT effectively usable with JPEG test files: + * + * NOT WRITABLE to JPEG: + * - MediaCreateDate: Can be read from video files but not written to JPEG + * - DateTimeCreated: Read-only tag in JPEG format + * - DateTimeUTC: Cannot be written to JPEG files + * - SonyDateTime2: Proprietary Sony tag, not writable to JPEG + * - SubSecMediaCreateDate: Tag not defined for JPEG format + * - SourceImageCreateTime: Non-standard insta360 tag, not writable to JPEG + * + * WRITABLE but NOT READABLE from JPEG: + * - SubSecDateTimeOriginal: Can be written but not read back from JPEG + * - SubSecCreateDate: Can be written but not read back from JPEG + * + * EFFECTIVELY TESTABLE TAGS (writable and readable): + * - DateTimeOriginal ✓ + * - CreateDate ✓ + * - CreationDate ✓ + * - GPSDateTime ✓ + * + * The metadata service correctly handles non-readable tags and will fall back to + * file timestamps when only non-readable tags are present. + */ + + describe('Date tag priority order', () => { + it('should respect the complete date tag priority order', async () => { + // Test cases using only EFFECTIVELY TESTABLE tags (writable AND readable from JPEG) + const testCases = [ + { + name: 'DateTimeOriginal has highest priority among testable tags', + exifData: { + DateTimeOriginal: '2023:04:04 04:00:00', // TESTABLE - highest priority among readable tags + CreateDate: '2023:05:05 05:00:00', // TESTABLE + CreationDate: '2023:07:07 07:00:00', // TESTABLE + GPSDateTime: '2023:10:10 10:00:00', // TESTABLE + }, + expectedDate: '2023-04-04T04:00:00.000Z', + }, + { + name: 'CreateDate when DateTimeOriginal missing', + exifData: { + CreateDate: '2023:05:05 05:00:00', // TESTABLE + CreationDate: '2023:07:07 07:00:00', // TESTABLE + GPSDateTime: '2023:10:10 10:00:00', // TESTABLE + }, + expectedDate: '2023-05-05T05:00:00.000Z', + }, + { + name: 'CreationDate when standard EXIF tags missing', + exifData: { + CreationDate: '2023:07:07 07:00:00', // TESTABLE + GPSDateTime: '2023:10:10 10:00:00', // TESTABLE + }, + expectedDate: '2023-07-07T07:00:00.000Z', + }, + { + name: 'GPSDateTime when no other testable date tags present', + exifData: { + GPSDateTime: '2023:10:10 10:00:00', // TESTABLE + Make: 'SONY', + }, + expectedDate: '2023-10-10T10:00:00.000Z', + }, + ]; + + for (const testCase of testCases) { + const { imageBytes, filename } = await createTestImageWithExif( + `${testCase.name.replaceAll(/\s+/g, '-').toLowerCase()}.jpg`, + testCase.exifData, + ); + + const asset = await utils.createAsset(admin.accessToken, { + assetData: { + filename, + bytes: imageBytes, + }, + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); + + const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) }); + + expect(assetInfo.exifInfo?.dateTimeOriginal, `Failed for: ${testCase.name}`).toBeDefined(); + expect( + new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime(), + `Date mismatch for: ${testCase.name}`, + ).toBe(new Date(testCase.expectedDate).getTime()); + } + }); + }); + + describe('Edge cases for date tag handling', () => { + it('should fall back to file timestamps with GPSDateStamp alone', async () => { + const { imageBytes, filename } = await createTestImageWithExif('gps-datestamp-only.jpg', { + GPSDateStamp: '2023:08:08', // Date-only tag, should not be used for dateTimeOriginal + // Intentionally no GPSTimeStamp + // Exclude all other date tags + SubSecDateTimeOriginal: undefined, + DateTimeOriginal: undefined, + SubSecCreateDate: undefined, + SubSecMediaCreateDate: undefined, + CreateDate: undefined, + MediaCreateDate: undefined, + CreationDate: undefined, + DateTimeCreated: undefined, + TimeCreated: undefined, + GPSDateTime: undefined, + DateTimeUTC: undefined, + }); + + const oldDate = new Date('2020-01-01T00:00:00.000Z'); + const asset = await utils.createAsset(admin.accessToken, { + assetData: { + filename, + bytes: imageBytes, + }, + fileCreatedAt: oldDate.toISOString(), + fileModifiedAt: oldDate.toISOString(), + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); + + const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) }); + + expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined(); + // Should fall back to file timestamps, which we set to 2020-01-01 + expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe( + new Date('2020-01-01T00:00:00.000Z').getTime(), + ); + }); + + it('should handle all testable date tags present to verify complete priority order', async () => { + const { imageBytes, filename } = await createTestImageWithExif('all-testable-date-tags.jpg', { + // All TESTABLE date tags to JPEG format (writable AND readable) + DateTimeOriginal: '2023:04:04 04:00:00', // TESTABLE - highest priority among readable tags + CreateDate: '2023:05:05 05:00:00', // TESTABLE + CreationDate: '2023:07:07 07:00:00', // TESTABLE + GPSDateTime: '2023:10:10 10:00:00', // TESTABLE + // Note: Excluded non-testable tags: + // SubSec tags: writable but not readable from JPEG + // Non-writable tags: MediaCreateDate, DateTimeCreated, DateTimeUTC, SonyDateTime2, etc. + // Time-only/date-only tags: already excluded from EXIF_DATE_TAGS + }); + + const asset = await utils.createAsset(admin.accessToken, { + assetData: { + filename, + bytes: imageBytes, + }, + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); + + const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) }); + + expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined(); + // Should use DateTimeOriginal as it has the highest priority among testable tags + expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe( + new Date('2023-04-04T04:00:00.000Z').getTime(), + ); + }); + + it('should use CreationDate when SubSec tags are missing', async () => { + const { imageBytes, filename } = await createTestImageWithExif('creation-date-priority.jpg', { + CreationDate: '2023:07:07 07:00:00', // WRITABLE + GPSDateTime: '2023:10:10 10:00:00', // WRITABLE + // Note: DateTimeCreated, DateTimeUTC, SonyDateTime2 are NOT writable to JPEG + // Note: TimeCreated and GPSDateStamp are excluded from EXIF_DATE_TAGS (time-only/date-only) + // Exclude SubSec and standard EXIF tags + SubSecDateTimeOriginal: undefined, + DateTimeOriginal: undefined, + SubSecCreateDate: undefined, + CreateDate: undefined, + }); + + const asset = await utils.createAsset(admin.accessToken, { + assetData: { + filename, + bytes: imageBytes, + }, + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); + + const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) }); + + expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined(); + // Should use CreationDate when available + expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe( + new Date('2023-07-07T07:00:00.000Z').getTime(), + ); + }); + + it('should skip invalid date formats and use next valid tag', async () => { + const { imageBytes, filename } = await createTestImageWithExif('invalid-date-handling.jpg', { + // Note: Testing invalid date handling with only WRITABLE tags + GPSDateTime: '2023:10:10 10:00:00', // WRITABLE - Valid date + CreationDate: '2023:13:13 13:00:00', // WRITABLE - Valid date + // Note: TimeCreated excluded (time-only), DateTimeCreated not writable to JPEG + // Exclude other date tags + SubSecDateTimeOriginal: undefined, + DateTimeOriginal: undefined, + SubSecCreateDate: undefined, + CreateDate: undefined, + }); + + const asset = await utils.createAsset(admin.accessToken, { + assetData: { + filename, + bytes: imageBytes, + }, + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); + + const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) }); + + expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined(); + // Should skip invalid dates and use the first valid one (GPSDateTime) + expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe( + new Date('2023-10-10T10:00:00.000Z').getTime(), + ); + }); + }); + }); + }); + describe('POST /assets/exist', () => { it('ignores invalid deviceAssetIds', async () => { const response = await utils.checkExistingAssets(user1.accessToken, { diff --git a/e2e/src/api/specs/auth.e2e-spec.ts b/e2e/src/api/specs/auth.e2e-spec.ts deleted file mode 100644 index 0f407f4ba7..0000000000 --- a/e2e/src/api/specs/auth.e2e-spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { LoginResponseDto, login, signUpAdmin } from '@immich/sdk'; -import { loginDto, signupDto } from 'src/fixtures'; -import { errorDto, loginResponseDto, signupResponseDto } from 'src/responses'; -import { app, utils } from 'src/utils'; -import request from 'supertest'; -import { beforeEach, describe, expect, it } from 'vitest'; - -const { email, password } = signupDto.admin; - -describe(`/auth/admin-sign-up`, () => { - beforeEach(async () => { - await utils.resetDatabase(); - }); - - describe('POST /auth/admin-sign-up', () => { - it(`should sign up the admin`, async () => { - const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin); - expect(status).toBe(201); - expect(body).toEqual(signupResponseDto.admin); - }); - - it('should not allow a second admin to sign up', async () => { - await signUpAdmin({ signUpDto: signupDto.admin }); - - const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.alreadyHasAdmin); - }); - }); -}); - -describe('/auth/*', () => { - let admin: LoginResponseDto; - - beforeEach(async () => { - await utils.resetDatabase(); - await signUpAdmin({ signUpDto: signupDto.admin }); - admin = await login({ loginCredentialDto: loginDto.admin }); - }); - - describe(`POST /auth/login`, () => { - it('should reject an incorrect password', async () => { - const { status, body } = await request(app).post('/auth/login').send({ email, password: 'incorrect' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.incorrectLogin); - }); - - it('should accept a correct password', async () => { - const { status, body, headers } = await request(app).post('/auth/login').send({ email, password }); - expect(status).toBe(201); - expect(body).toEqual(loginResponseDto.admin); - - const token = body.accessToken; - expect(token).toBeDefined(); - - const cookies = headers['set-cookie']; - expect(cookies).toHaveLength(3); - expect(cookies[0].split(';').map((item) => item.trim())).toEqual([ - `immich_access_token=${token}`, - 'Max-Age=34560000', - 'Path=/', - expect.stringContaining('Expires='), - 'HttpOnly', - 'SameSite=Lax', - ]); - expect(cookies[1].split(';').map((item) => item.trim())).toEqual([ - 'immich_auth_type=password', - 'Max-Age=34560000', - 'Path=/', - expect.stringContaining('Expires='), - 'HttpOnly', - 'SameSite=Lax', - ]); - expect(cookies[2].split(';').map((item) => item.trim())).toEqual([ - 'immich_is_authenticated=true', - 'Max-Age=34560000', - 'Path=/', - expect.stringContaining('Expires='), - 'SameSite=Lax', - ]); - }); - }); - - describe('POST /auth/validateToken', () => { - it('should reject an invalid token', async () => { - const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.invalidToken); - }); - - it('should accept a valid token', async () => { - const { status, body } = await request(app) - .post(`/auth/validateToken`) - .send({}) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(200); - expect(body).toEqual({ authStatus: true }); - }); - }); - - describe('POST /auth/change-password', () => { - it('should require the current password', async () => { - const { status, body } = await request(app) - .post(`/auth/change-password`) - .send({ password: 'wrong-password', newPassword: 'Password1234' }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.wrongPassword); - }); - - it('should change the password', async () => { - const { status } = await request(app) - .post(`/auth/change-password`) - .send({ password, newPassword: 'Password1234' }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(200); - - await login({ - loginCredentialDto: { - email: 'admin@immich.cloud', - password: 'Password1234', - }, - }); - }); - }); - - describe('POST /auth/logout', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post(`/auth/logout`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should logout the user', async () => { - const { status, body } = await request(app) - .post(`/auth/logout`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(200); - expect(body).toEqual({ - successful: true, - redirectUri: '/auth/login?autoLaunch=0', - }); - }); - }); -}); diff --git a/e2e/src/api/specs/memory.e2e-spec.ts b/e2e/src/api/specs/memory.e2e-spec.ts index d91a570f77..e5e2351738 100644 --- a/e2e/src/api/specs/memory.e2e-spec.ts +++ b/e2e/src/api/specs/memory.e2e-spec.ts @@ -6,7 +6,7 @@ import { createMemory, getMemory, } from '@immich/sdk'; -import { createUserDto, uuidDto } from 'src/fixtures'; +import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; @@ -17,7 +17,6 @@ describe('/memories', () => { let user: LoginResponseDto; let adminAsset: AssetMediaResponseDto; let userAsset1: AssetMediaResponseDto; - let userAsset2: AssetMediaResponseDto; let userMemory: MemoryResponseDto; beforeAll(async () => { @@ -25,10 +24,9 @@ describe('/memories', () => { admin = await utils.adminSetup(); user = await utils.userSetup(admin.accessToken, createUserDto.user1); - [adminAsset, userAsset1, userAsset2] = await Promise.all([ + [adminAsset, userAsset1] = await Promise.all([ utils.createAsset(admin.accessToken), utils.createAsset(user.accessToken), - utils.createAsset(user.accessToken), ]); userMemory = await createMemory( { @@ -43,121 +41,7 @@ describe('/memories', () => { ); }); - describe('GET /memories', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/memories'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - - describe('POST /memories', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/memories'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should validate data when type is on this day', async () => { - const { status, body } = await request(app) - .post('/memories') - .set('Authorization', `Bearer ${user.accessToken}`) - .send({ - type: 'on_this_day', - data: {}, - memoryAt: new Date(2021).toISOString(), - }); - - expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']), - ); - }); - - it('should create a new memory', async () => { - const { status, body } = await request(app) - .post('/memories') - .set('Authorization', `Bearer ${user.accessToken}`) - .send({ - type: 'on_this_day', - data: { year: 2021 }, - memoryAt: new Date(2021).toISOString(), - }); - - expect(status).toBe(201); - expect(body).toEqual({ - id: expect.any(String), - type: 'on_this_day', - data: { year: 2021 }, - createdAt: expect.any(String), - updatedAt: expect.any(String), - isSaved: false, - memoryAt: expect.any(String), - ownerId: user.userId, - assets: [], - }); - }); - - it('should create a new memory (with assets)', async () => { - const { status, body } = await request(app) - .post('/memories') - .set('Authorization', `Bearer ${user.accessToken}`) - .send({ - type: 'on_this_day', - data: { year: 2021 }, - memoryAt: new Date(2021).toISOString(), - assetIds: [userAsset1.id, userAsset2.id], - }); - - expect(status).toBe(201); - expect(body).toMatchObject({ - id: expect.any(String), - assets: expect.arrayContaining([ - expect.objectContaining({ id: userAsset1.id }), - expect.objectContaining({ id: userAsset2.id }), - ]), - }); - expect(body.assets).toHaveLength(2); - }); - - it('should create a new memory and ignore assets the user does not have access to', async () => { - const { status, body } = await request(app) - .post('/memories') - .set('Authorization', `Bearer ${user.accessToken}`) - .send({ - type: 'on_this_day', - data: { year: 2021 }, - memoryAt: new Date(2021).toISOString(), - assetIds: [userAsset1.id, adminAsset.id], - }); - - expect(status).toBe(201); - expect(body).toMatchObject({ - id: expect.any(String), - assets: [expect.objectContaining({ id: userAsset1.id })], - }); - expect(body.assets).toHaveLength(1); - }); - }); - describe('GET /memories/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/memories/${uuidDto.invalid}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .get(`/memories/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should require access', async () => { const { status, body } = await request(app) .get(`/memories/${userMemory.id}`) @@ -176,22 +60,6 @@ describe('/memories', () => { }); describe('PUT /memories/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/memories/${uuidDto.invalid}`).send({ isSaved: true }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .put(`/memories/${uuidDto.invalid}`) - .send({ isSaved: true }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should require access', async () => { const { status, body } = await request(app) .put(`/memories/${userMemory.id}`) @@ -218,23 +86,6 @@ describe('/memories', () => { }); describe('PUT /memories/:id/assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .put(`/memories/${userMemory.id}/assets`) - .send({ ids: [userAsset1.id] }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .put(`/memories/${uuidDto.invalid}/assets`) - .send({ ids: [userAsset1.id] }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should require access', async () => { const { status, body } = await request(app) .put(`/memories/${userMemory.id}/assets`) @@ -244,15 +95,6 @@ describe('/memories', () => { expect(body).toEqual(errorDto.noPermission); }); - it('should require a valid asset id', async () => { - const { status, body } = await request(app) - .put(`/memories/${userMemory.id}/assets`) - .send({ ids: [uuidDto.invalid] }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); - }); - it('should require asset access', async () => { const { status, body } = await request(app) .put(`/memories/${userMemory.id}/assets`) @@ -279,23 +121,6 @@ describe('/memories', () => { }); describe('DELETE /memories/:id/assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .delete(`/memories/${userMemory.id}/assets`) - .send({ ids: [userAsset1.id] }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .delete(`/memories/${uuidDto.invalid}/assets`) - .send({ ids: [userAsset1.id] }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should require access', async () => { const { status, body } = await request(app) .delete(`/memories/${userMemory.id}/assets`) @@ -305,15 +130,6 @@ describe('/memories', () => { expect(body).toEqual(errorDto.noPermission); }); - it('should require a valid asset id', async () => { - const { status, body } = await request(app) - .delete(`/memories/${userMemory.id}/assets`) - .send({ ids: [uuidDto.invalid] }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); - }); - it('should only remove assets in the memory', async () => { const { status, body } = await request(app) .delete(`/memories/${userMemory.id}/assets`) @@ -340,21 +156,6 @@ describe('/memories', () => { }); describe('DELETE /memories/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/memories/${uuidDto.invalid}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .delete(`/memories/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should require access', async () => { const { status, body } = await request(app) .delete(`/memories/${userMemory.id}`) diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/api/specs/oauth.e2e-spec.ts index 9e4d64892e..58fc43a2d5 100644 --- a/e2e/src/api/specs/oauth.e2e-spec.ts +++ b/e2e/src/api/specs/oauth.e2e-spec.ts @@ -227,6 +227,21 @@ describe(`/oauth`, () => { expect(user.storageLabel).toBe('user-username'); }); + it('should set the admin status from a role claim', async () => { + const callbackParams = await loginWithOAuth(OAuthUser.WITH_ROLE); + const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); + expect(status).toBe(201); + expect(body).toMatchObject({ + accessToken: expect.any(String), + userId: expect.any(String), + userEmail: 'oauth-with-role@immich.app', + isAdmin: true, + }); + + const user = await getMyUser({ headers: asBearerAuth(body.accessToken) }); + expect(user.isAdmin).toBe(true); + }); + it('should work with RS256 signed tokens', async () => { await setupOAuth(admin.accessToken, { enabled: true, diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts index 04ed8ca0a4..d9f8672c66 100644 --- a/e2e/src/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/api/specs/shared-link.e2e-spec.ts @@ -117,8 +117,25 @@ describe('/shared-links', () => { const resp = await request(shareUrl).get(`/${linkWithAssets.key}`); expect(resp.status).toBe(200); expect(resp.header['content-type']).toContain('text/html'); + expect(resp.text).toContain(` tag.includes('Date') || tag.includes('Time') || tag.includes('Created'), + ); + if (firstDateTag && metadata[firstDateTag]) { + console.log(` ✓ Verified ${firstDateTag}: ${metadata[firstDateTag]}`); + } + } catch (error) { + console.error(`Failed to create ${image.filename}:`, (error as Error).message); + } + } + + console.log('\nTest image generation complete!'); + console.log('Files created in:', targetDir); + console.log('\nTo test these images:'); + console.log(`cd ${targetDir} && exiftool -time:all -gps:all *.jpg`); +}; + +export { generateTestImages }; + +// Run the generator if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + generateTestImages().catch(console.error); +} diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index bb6d17a248..b14aedf895 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -116,6 +116,7 @@ export const deviceDto = { createdAt: expect.any(String), updatedAt: expect.any(String), current: true, + isPendingSyncReset: false, deviceOS: '', deviceType: '', }, diff --git a/e2e/src/setup/auth-server.ts b/e2e/src/setup/auth-server.ts index 575e97d291..489bda2ee4 100644 --- a/e2e/src/setup/auth-server.ts +++ b/e2e/src/setup/auth-server.ts @@ -12,6 +12,7 @@ export enum OAuthUser { NO_NAME = 'no-name', WITH_QUOTA = 'with-quota', WITH_USERNAME = 'with-username', + WITH_ROLE = 'with-role', } const claims = [ @@ -34,6 +35,12 @@ const claims = [ preferred_username: 'user-quota', immich_quota: 25, }, + { + sub: OAuthUser.WITH_ROLE, + email: 'oauth-with-role@immich.app', + email_verified: true, + immich_role: 'admin', + }, ]; const withDefaultClaims = (sub: string) => ({ @@ -64,7 +71,15 @@ const setup = async () => { claims: { openid: ['sub'], email: ['email', 'email_verified'], - profile: ['name', 'given_name', 'family_name', 'preferred_username', 'immich_quota', 'immich_username'], + profile: [ + 'name', + 'given_name', + 'family_name', + 'preferred_username', + 'immich_quota', + 'immich_username', + 'immich_role', + ], }, features: { jwtUserinfo: { diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 1d5004d385..3fcc4ab552 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -60,6 +60,7 @@ import { io, type Socket } from 'socket.io-client'; import { loginDto, signupDto } from 'src/fixtures'; import { makeRandomImage } from 'src/generators'; import request from 'supertest'; +export type { Emitter } from '@socket.io/component-emitter'; type CommandResponse = { stdout: string; stderr: string; exitCode: number | null }; type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden'; @@ -84,10 +85,10 @@ export const immichAdmin = (args: string[]) => export const specialCharStrings = ["'", '"', ',', '{', '}', '*']; export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; -const executeCommand = (command: string, args: string[]) => { +const executeCommand = (command: string, args: string[], options?: { cwd?: string }) => { let _resolve: (value: CommandResponse) => void; const promise = new Promise((resolve) => (_resolve = resolve)); - const child = spawn(command, args, { stdio: 'pipe' }); + const child = spawn(command, args, { stdio: 'pipe', cwd: options?.cwd }); let stdout = ''; let stderr = ''; @@ -153,19 +154,19 @@ export const utils = { tables = tables || [ // TODO e2e test for deleting a stack, since it is quite complex - 'asset_stack', - 'libraries', - 'shared_links', + 'stack', + 'library', + 'shared_link', 'person', - 'albums', - 'assets', - 'asset_faces', + 'album', + 'asset', + 'asset_face', 'activity', - 'api_keys', - 'sessions', - 'users', + 'api_key', + 'session', + 'user', 'system_metadata', - 'tags', + 'tag', ]; const sql: string[] = []; @@ -174,7 +175,7 @@ export const utils = { if (table === 'system_metadata') { sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`); } else { - sql.push(`DELETE FROM ${table} CASCADE;`); + sql.push(`DELETE FROM "${table}" CASCADE;`); } } @@ -450,7 +451,7 @@ export const utils = { return; } - await client.query('INSERT INTO asset_faces ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]); + await client.query('INSERT INTO asset_face ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]); }, setPersonThumbnail: async (personId: string) => { diff --git a/e2e/test-assets b/e2e/test-assets index 8885d6d01c..37f60ea537 160000 --- a/e2e/test-assets +++ b/e2e/test-assets @@ -1 +1 @@ -Subproject commit 8885d6d01c12242785b6ea68f4a277334f60bc90 +Subproject commit 37f60ea537c0228f5f92e4f42dc42f0bb39a6d7f diff --git a/i18n/af.json b/i18n/af.json index 55555c8398..b3729903ec 100644 --- a/i18n/af.json +++ b/i18n/af.json @@ -4,6 +4,7 @@ "account_settings": "Rekeninginstellings", "acknowledge": "Erken", "action": "Aksie", + "action_common_update": "Opdateur", "actions": "Aksies", "active": "Aktief", "activity": "Aktiwiteite", @@ -13,6 +14,7 @@ "add_a_location": "Voeg 'n ligging by", "add_a_name": "Voeg 'n naam by", "add_a_title": "Voeg 'n titel by", + "add_endpoint": "Voeg Koppelvlakpunt by", "add_exclusion_pattern": "Voeg uitsgluitingspatrone by", "add_import_path": "Voeg invoerpad by", "add_location": "Voeg ligging by", @@ -20,26 +22,30 @@ "add_partner": "Voeg vennoot by", "add_path": "Voeg pad by", "add_photos": "Voeg foto's by", + "add_tag": "Voeg tag by", "add_to": "Voeg byâ€Ļ", "add_to_album": "Voeg na album", - "add_to_shared_album": "Voeg na gedeelde album", + "add_to_album_bottom_sheet_added": "By {album} bygevoeg", + "add_to_album_bottom_sheet_already_exists": "Reeds in {album}", + "add_to_shared_album": "Voeg toe aan gedeelde album", "add_url": "Voeg URL by", - "added_to_archive": "By argief gevoeg", - "added_to_favorites": "By gunstelinge gevoeg", - "added_to_favorites_count": "Het {count, number} by gunstelinge gevoeg", + "added_to_archive": "By argief toegevoegd", + "added_to_favorites": "By gunstelinge toegevoegd", + "added_to_favorites_count": "Het {count, number} by gunstelinge toegevoegd", "admin": { "add_exclusion_pattern_description": "Voeg uitsluitingspatrone by. Globbing met *, ** en ? word ondersteun. Om alle lÃĒers in enige lÃĒergids genaamd \"Raw\" te ignoreer, gebruik \"**/Raw/**\". Om alle lÃĒers wat op \".tif\" eindig, te ignoreer, gebruik \"**/*.tif\". Om 'n absolute pad te ignoreer, gebruik \"/path/to/ignore/**\".", + "admin_user": "Admin gebruiker", "asset_offline_description": "Hierdie eksterne biblioteekbate word nie meer op skyf gevind nie en is na die asblik geskuif. As die lÃĒer binne die biblioteek geskuif is, gaan jou tydlyn na vir die nuwe ooreenstemmende bate. Om hierdie bate te herstel, maak asseblief seker dat die lÃĒerpad hieronder deur Immich verkry kan word en skandeer die biblioteek.", "authentication_settings": "Verifikasie instellings", "authentication_settings_description": "Bestuur wagwoord, OAuth en ander verifikasie instellings", "authentication_settings_disable_all": "Is jy seker jy wil alle aanmeldmetodes deaktiveer? Aanmelding sal heeltemal gedeaktiveer word.", "authentication_settings_reenable": "Om te heraktiveer, gebruik 'n Server Command.", "background_task_job": "Agtergrondtake", - "backup_database": "Rugsteun databasis", + "backup_database": "Skep DatastortlÃĒer", "backup_database_enable_description": "Aktiveer databasisrugsteun", "backup_keep_last_amount": "Aantal vorige rugsteune om te hou", "backup_settings": "Rugsteun instellings", - "backup_settings_description": "Bestuur databasis rugsteun instellings", + "backup_settings_description": "Bestuur databasis rugsteun instellings.", "cleared_jobs": "Poste gevee vir: {job}", "config_set_by_file": "Config word tans deur 'n konfigurasielÃĒer gestel", "confirm_delete_library": "Is jy seker jy wil {library}-biblioteek uitvee?", @@ -47,6 +53,7 @@ "confirm_email_below": "Om te bevestig, tik \"{email}\" hieronder", "confirm_reprocess_all_faces": "Is jy seker jy wil alle gesigte herverwerk? Dit sal ook genoemde mense skoonmaak.", "confirm_user_password_reset": "Is jy seker jy wil {user} se wagwoord terugstel?", + "confirm_user_pin_code_reset": "Is jy seker jy wil {user} se PIN kode herstel?", "create_job": "Skep werk", "cron_expression": "Cron uitdrukking", "cron_expression_description": "Stel die skanderingsinterval in met die cron-formaat. Vir meer inligting verwys asseblief na bv. Crontab Guru", @@ -56,10 +63,14 @@ "exclusion_pattern_description": "Met uitsluitingspatrone kan jy lÃĒers en vouers ignoreer wanneer jy jou biblioteek skandeer. Dit is nuttig as jy vouers het wat lÃĒers bevat wat jy nie wil invoer nie, soos RAW-lÃĒers.", "external_library_management": "Eksterne Biblioteekbestuur", "face_detection": "Gesig deteksie", + "face_detection_description": "Detecteer die gesigte in media deur middel van masjienleer. Vir videos word slegs die duimnaelskets oorweeg. “Herlaai” (ver)werk al die media weer. “Stel terug” verwyder boonop alle huidige gesigdata. “Onverwerk” plaas bates in die tou wat nog nie verwerk is nie. Gedekte gesigte sal nÃĄ voltooiing van Gesigdetectie vir Gesigherkenning in die tou geplaas word, om hulle in bestaande of nuwe persone te groepeer.", + "facial_recognition_job_description": "Groepeer gesigte in mense in. Die stap is vinniger nadat Gesig Deteksie klaar is. \"Herstel\" (her-)groepeer alle gesigte. \"Vermiste\" plaas gesigte in ry wat nie 'n persoon gekoppel het nie.", "failed_job_command": "Opdrag {command} het misluk vir werk: {job}", "force_delete_user_warning": "WAARSKUWING: Dit sal onmiddellik die gebruiker en alle bates verwyder. Dit kan nie ontdoen word nie en die lÃĒers kan nie herstel word nie.", "image_format": "Formaat", "image_format_description": "WebP produseer kleiner lÃĒers as JPEG, maar is stadiger om te enkodeer.", + "image_fullsize_description": "Vol grote prent met geen metadata, gebruik wanner ingezoem", + "image_fullsize_enabled": "Skakel aan vol grote prent generasie", "image_prefer_embedded_preview": "Verkies ingebedde voorskou", "image_prefer_wide_gamut": "Verkies wide gamut", "image_prefer_wide_gamut_setting_description": "Gebruik Display P3 vir kleinkiekies. Dit behou die lewendheid van beelde met wye kleurruimtes beter, maar beelde kan anders verskyn op ou apparate met 'n ou blaaierweergawe. sRGB-beelde gebruik steeds sRGB om kleurverskuiwings te voorkom.", @@ -77,8 +88,99 @@ "job_concurrency": "{job} gelyktydigheid", "job_created": "Taak gemaak", "job_not_concurrency_safe": "Hierdie taak kan nie gelyktydig uitgevoer word nie.", - "job_settings": "Agtergrondtaakinstellings" + "job_settings": "Agtergrondtaakinstellings", + "job_settings_description": "Bestuur werkgelyktydigheid", + "job_status": "Werkstatus", + "library_created": "Biblioteek geskep: {library}", + "library_deleted": "Biblioteek verwyder", + "library_import_path_description": "Spesifiseer 'n leer om in te neem. Hierdie leer, en al die sub leers, gaan geskandeer for vir prente en videos.", + "library_scanning": "Periodieke Skandering", + "library_scanning_description": "Stel periodieke skandering van biblioteek in", + "library_scanning_enable_description": "Aktiveer periodieke biblioteekskandering", + "library_settings": "Eksterne Biblioteek", + "map_settings": "Kaart", + "migration_job": "Migrasie", + "oauth_settings": "OAuth", + "transcoding_acceleration_vaapi": "VAAPI" }, + "administration": "Administrasie", + "advanced": "Gevorderde", + "albums": "Albums", + "all": "Alle", + "anti_clockwise": "Anti-kloksgewys", + "archive": "Argief", + "asset_skipped": "Oorgeslaan", + "asset_uploaded": "Opgelaai", + "asset_uploading": "Oplaaiâ€Ļ", + "assets": "Bates", + "back": "Terug", + "backward": "Agteruit", + "build": "Bou", + "camera": "Kamera", + "cancel": "Kanselleer", + "city": "Stad", + "clockwise": "Kloksgewys", + "close": "Maak toe", + "color": "Kleur", + "confirm": "Bevestig", + "contain": "Bevat", + "context": "Konteks", + "continue": "Gaan voort", + "country": "Land", + "cover": "Bedek", + "create": "Skep", + "created": "Geskep", + "dark": "Donker", + "day": "Dag", + "delete": "Verwyder", + "description": "Beskrywing", + "details": "Besonderhede", + "direction": "Rigting", + "discover": "Ontdek", + "documentation": "Dokumentasie", + "done": "Klaar", + "download": "Aflaai", + "download_settings": "Aflaai", + "duplicates": "Duplikate", + "duration": "Duur", + "edit": "Wysig", + "edited": "Gewysigd", "search_by_description": "Soek by beskrywing", - "search_by_description_example": "Stapdag in Sapa" + "search_by_description_example": "Stapdag in Sapa", + "version": "Weergawe", + "version_announcement_closing": "Jou friend, Alex", + "version_history": "Weergawegeskiedenis", + "version_history_item": "{version} geinstaleerd op {date}", + "video": "Video", + "videos": "Video's", + "view": "Bekyk", + "view_album": "Bekyk Album", + "view_all": "Bekyk alle", + "view_all_users": "Bekyk alle gebruikers", + "view_in_timeline": "Bekyk in tydlyn", + "view_link": "Bekyk skakel", + "view_links": "Bekyk skakels", + "view_name": "Bekyk", + "view_next_asset": "Bekyk volgende bate", + "view_previous_asset": "Bekyk vorige bate", + "view_qr_code": "Bekyk QR-kode", + "view_stack": "Bekyk stapel", + "view_user": "Bekyk gebruiker", + "viewer_remove_from_stack": "Verwyder van stapel", + "viewer_stack_use_as_main_asset": "Gebruik as hoofbate", + "viewer_unstack": "Ontstapel", + "visibility_changed": "Sigbaarheid verander voor {count, plural, one {# person} other {# people}}", + "waiting": "Wag", + "warning": "Waaskuwing", + "week": "Week", + "welcome": "Welkom", + "welcome_to_immich": "Welkom by Immich", + "wifi_name": "Wi-Fi Naam", + "wrong_pin_code": "Verkeerde PIN-kode", + "year": "Jaar", + "years_ago": "{years, plural, one {# year} other {# years}} gelede", + "yes": "Ja", + "you_dont_have_any_shared_links": "Jy het geen gedeelde skakels", + "your_wifi_name": "Jou Wi-Fi naam", + "zoom_image": "Vergroot Prent" } diff --git a/i18n/ar.json b/i18n/ar.json index 67f7752ae1..6042c8f82f 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -1,19 +1,20 @@ { - "about": "Ų…Ų† Ų†Ø­Ų†", - "account": "Ø§Ų„Ø­ØŗØ§Ø¨", + "about": "ØšŲ†", + "account": "Ø­ØŗØ§Ø¨", "account_settings": "ØĨؚداداØĒ Ø§Ų„Ø­ØŗØ§Ø¨", "acknowledge": "ØŖŲØ¯ØąŲƒ Ø°Ų„Ųƒ", - "action": "Ø§Ų„ØĒØ­ŲƒŲ…", + "action": "ØšŲ…Ų„ŲŠØŠ", "action_common_update": "ØĒØ­Ø¯ŲŠØĢ", - "actions": "Ø§Ų„ØšŲ…Ų„ŲŠØ§ØĒ", + "actions": "ØšŲ…Ų„ŲŠØ§ØĒ", "active": "Ų†Ø´Øˇ", - "activity": "Ø§Ų„Ų†Ø´Ø§Øˇ", + "activity": "Ų†Ø´Ø§Øˇ", "activity_changed": "Ø§Ų„Ų†Ø´Ø§Øˇ {enabled, select, true {Ų…ŲŲŲ’ØšŲ„} other {Ų…ØšØˇŲ‘Ų„}}", "add": "ØĨØļØ§ŲØŠ", "add_a_description": "ØĨØļØ§ŲØŠ ؈Øĩ؁", "add_a_location": "ØĨØļØ§ŲØŠ Ų…ŲˆŲ‚Øš", "add_a_name": "ØĨØļØ§ŲØŠ ØĨØŗŲ…", "add_a_title": "ØĨØļØ§ŲØŠ ØšŲ†ŲˆØ§Ų†", + "add_endpoint": "اØļ؁ Ų†Ų‚ØˇØŠ Ų†Ų‡Ø§ŲŠØŠ", "add_exclusion_pattern": "ØĨØļØ§ŲØŠ Ų†Ų…Øˇ ØĨØŗØĒØĢŲ†Ø§ØĄ", "add_import_path": "ØĨØļØ§ŲØŠ Ų…ØŗØ§Øą Ø§Ų„ØĨØŗØĒŲŠØąØ§Ø¯", "add_location": "ØĨØļØ§ŲØŠ Ų…ŲˆŲ‚Øš", @@ -21,28 +22,30 @@ "add_partner": "ØŖØļ؁ Ø´ØąŲŠŲƒŲ‹Ø§", "add_path": "ØĨØļØ§ŲØŠ Ų…ØŗØ§Øą", "add_photos": "ØĨØļØ§ŲØŠ ØĩŲˆØą", + "add_tag": "اØļ؁ ØšŲ„Ø§Ų…ØŠ", "add_to": "ØĨØļØ§ŲØŠ ØĨŲ„Ų‰â€Ļ", "add_to_album": "ØĨØļØ§ŲØŠ ØĨŲ„Ų‰ ØŖŲ„Ø¨ŲˆŲ…", "add_to_album_bottom_sheet_added": "ØĒŲ…ØĒ Ø§Ų„Ø§ØļØ§ŲØŠ{album}", "add_to_album_bottom_sheet_already_exists": "Ų…ŲˆØŦŲˆØ¯ØŠ Ų…ØŗØ¨Ų‚Ø§ {album}", - "add_to_shared_album": "ØĨØļØ§ŲØŠ ØĨŲ„Ų‰ ØŖŲ„Ø¨ŲˆŲ… Ų…Ø´ØĒØąŲƒ", + "add_to_shared_album": "ØĨØļØ§ŲØŠ ØĨŲ„Ų‰ ØŖŲ„Ø¨ŲˆŲ… Ų…Ø´Ø§ØąŲƒ", "add_url": "ØĨØļØ§ŲØŠ ØąØ§Ø¨Øˇ", "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_settings": "ØĨؚداداØĒ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", - "backup_settings_description": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ", + "backup_database": "Ø§Ų†Ø´Ø§ØĄ ØĒŲØąŲŠØē Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ", + "backup_database_enable_description": "ØĒŲ…ŲƒŲŠŲ† ØĒŲØąŲŠØē Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ", + "backup_keep_last_amount": "Ų…Ų‚Ø¯Ø§Øą Ø§Ų„ØĒŲØąŲŠØēاØĒ Ø§Ų„ØŗØ§Ø¨Ų‚ØŠ Ų„Ų„Ø§Ø­ØĒŲØ§Ø¸ Ø¨Ų‡Ø§", + "backup_settings": "ØĨؚداداØĒ ØĒŲØąŲŠØē Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ", + "backup_settings_description": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ ØĒŲØąŲŠØē Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ.", "cleared_jobs": "ØĒŲ… ØĨØŽŲ„Ø§ØĄ Ų…Ų‡Ø§Ų…: {job}", "config_set_by_file": "Ø§Ų„ØĨؚداداØĒ Ø­Ø§Ų„ŲŠŲ‹Ø§ Ų…ØšŲŠŲ†ØŠ ØšŲ† ØˇØąŲŠŲ‚ ؅؄؁ Ø§Ų„Ø§ØšØ¯Ø§Ø¯Ø§ØĒ", "confirm_delete_library": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ Ø­Ø°Ų Ų…ŲƒØĒب؊ {library}؟", @@ -50,6 +53,7 @@ "confirm_email_below": "Ų„Ų„ØĒØŖŲƒŲŠØ¯ØŒ Ø§ŲƒØĒب \"{email}\" Ø¨Ø§Ų„ØŖØŗŲŲ„", "confirm_reprocess_all_faces": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ ØĨؚاد؊ Ų…ØšØ§Ų„ØŦØŠ ØŦŲ…ŲŠØš Ø§Ų„ŲˆØŦŲˆŲ‡ØŸ ØŗŲŠØŽŲ„ŲŠ Ų‡Ø°Ø§ ŲƒŲ„ Ø§Ų„ØŖØ´ØŽØ§Øĩ Ø§Ų„Ø°ŲŠŲ† ØŗŲŽŲ…ŲŠØĒŲŽŲ‡Ų….", "confirm_user_password_reset": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ ØĨؚاد؊ ØĒØšŲŠŲŠŲ† ŲƒŲ„Ų…ØŠ Ų…ØąŲˆØą {user}؟", + "confirm_user_pin_code_reset": "Ų‡Ų„ Ø§Ų†ØĒ Ų…ØĒØ§ŲƒØ¯ Ų…Ų† اؚاد؊ ØļØ¨Øˇ ØąŲ…Ø˛ PIN Ø§Ų„ØŽØ§Øĩ ب {user}؟", "create_job": "ØĨŲ†Ø´Ø§ØĄ ŲˆØ¸ŲŠŲØŠ", "cron_expression": "ØĒØšØ¨ŲŠØą Cron", "cron_expression_description": "اØļØ¨Øˇ Ø§Ų„ŲØ§ØĩŲ„ Ø§Ų„Ø˛Ų…Ų†ŲŠ Ų„Ų„ŲØ­Øĩ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… ØĒŲ†ØŗŲŠŲ‚ cron. Ų„Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„Ų…ØšŲ„ŲˆŲ…Ø§ØĒ ŲŠŲØąØŦŲ‰ Ø§Ų„ØąØŦŲˆØš ØĨŲ„Ų‰ Crontab Guru ØšŲ„Ų‰ ØŗØ¨ŲŠŲ„ Ø§Ų„Ų…ØĢØ§Ų„", @@ -65,10 +69,15 @@ "force_delete_user_warning": "ØĒØ­Ø°ŲŠØą: ØŗŲŠØ¤Ø¯ŲŠ Ø°Ų„Ųƒ ØĨŲ„Ų‰ ØĨØ˛Ø§Ų„ØŠ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų… ؈ØŦŲ…ŲŠØš Ų…Ø­ØĒŲˆŲŠØ§ØĒŲ‡ ØšŲ„Ų‰ Ø§Ų„ŲŲˆØą. Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų„ØĒØąØ§ØŦØš ØšŲ† Ų‡Ø°Ø§ Ø§Ų„ØĨØŦØąØ§ØĄ ŲˆŲ„Ø§ ŲŠŲ…ŲƒŲ† Ø§ØŗØĒØąØ¯Ø§Ø¯ Ø§Ų„Ų…Ų„ŲØ§ØĒ.", "image_format": "Ø§Ų„ØĒŲ†ØŗŲŠŲ‚", "image_format_description": "ŲŠŲŲ†ØĒØŦ WebP Ų…Ų„ŲØ§ØĒ ØŖØĩØēØą Ø­ØŦŲ…Ų‹Ø§ Ų…Ų† Ų…Ų„ŲØ§ØĒ JPEG، ŲˆŲ„ŲƒŲ†Ų‡ ØŖØ¨ØˇØŖ ؁؊ ØšŲ…Ų„ŲŠØŠ Ø§Ų„ØĒØąŲ…ŲŠØ˛.", + "image_fullsize_description": "ØĩŲˆØąØŠ بحØŦŲ… ŲƒØ§Ų…Ų„ Ų…Øš Ø§Ø˛Ø§Ų„ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ Ø§Ų„ŲˆØĩŲŲŠØŠØŒ ØĒØŗØĒØŽØ¯Ų… ØšŲ†Ø¯ Ø§Ų„ØĒŲƒØ¨ŲŠØą", + "image_fullsize_enabled": "ØĒŲ…ŲƒŲŠŲ† ØĒŲˆŲ„ŲŠØ¯ Ø§Ų„ØĩŲˆØą بحØŦŲ… ŲƒØ§Ų…Ų„", + "image_fullsize_enabled_description": "ØĒŲˆŲ„ŲŠØ¯ ØĩŲˆØą بحØŦŲ… ŲƒØ§Ų…Ų„ Ų„Ų„Øĩ؊Øē Ø§Ų„ØēŲŠØą ØĩØ¯ŲŠŲ‚ØŠ Ų„Ų„ŲˆŲŠØ¨. ØšŲ†Ø¯ ØĒŲØšŲŠŲ„ \"ØĒ؁ØļŲŠŲ„ Ø§Ų„ØšØąØļ Ø§Ų„Ų…Ø¯Ų…ØŦ\" ، Ø§Ų„ØšØąŲˆØļ Ø§Ų„Ų…Ø¯Ų…ØŦŲ‡ ØĒØŗØĒØŽØ¯Ų… Ø¨Ø´ŲƒŲ„ Ų…Ø¨Ø§Ø´Øą Ø¨Ø¯ŲˆŲ† ØĒØ­ŲˆŲŠŲ„. Ų„Ø§ ŲŠØ¤ØĢØą ØšŲ„Ų‰ Ø§Ų„Øĩ؊Øē Ø§Ų„ØĩØ¯ŲŠŲ‚ØŠ Ų„Ų„ŲˆŲŠØ¨ Ų…ØĢŲ„ JPEG.", + "image_fullsize_quality_description": "ØĩŲˆØą Ø¨Ø¯Ų‚ØŠ ŲƒØ§Ų…Ų„ØŠ Ų…Ų† ŲĄ-ŲĄŲ Ų . Ø§Ų„Ø§ØšŲ„Ų‰ Ø§ŲØļŲ„ ŲˆŲ„ŲƒŲ† ŲŠŲ†ØĒØŦ Ų…Ų„ŲØ§ØĒ بحØŦŲ… Ø§ŲƒØ¨Øą.", + "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_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_preview_title": "ØĨؚداداØĒ Ø§Ų„Ų…ØšØ§ŲŠŲ†ØŠ", @@ -91,18 +100,18 @@ "library_created": "ØĒŲ… ØĨŲ†Ø´Ø§ØĄ Ø§Ų„Ų…ŲƒØĒب؊: {library}", "library_deleted": "ØĒŲ… Ø­Ø°Ų Ø§Ų„Ų…ŲƒØĒب؊", "library_import_path_description": "حدد Ų…ØŦŲ„Ø¯Ų‹Ø§ Ų„Ų„Ø§ØŗØĒŲŠØąØ§Ø¯. ØŗŲŠØĒŲ… ŲØ­Øĩ Ų‡Ø°Ø§ Ø§Ų„Ų…ØŦŲ„Ø¯ØŒ Ø¨Ų…Ø§ ؁؊ Ø°Ų„Ųƒ Ø§Ų„Ų…ØŦŲ„Ø¯Ø§ØĒ Ø§Ų„ŲØąØšŲŠØŠØŒ بحØĢŲ‹Ø§ ØšŲ† Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ.", - "library_scanning": "Ø§Ų„ŲØ­Øĩ Ø§Ų„Ø¯ŲˆØąŲŠ", - "library_scanning_description": "ØĨؚداد ŲØ­Øĩ Ø§Ų„Ų…ŲƒØĒب؊ Ø§Ų„Ø¯ŲˆØąŲŠ", - "library_scanning_enable_description": "ØĒŲØšŲŠŲ„ ŲØ­Øĩ Ø§Ų„Ų…ŲƒØĒب؊ Ø§Ų„Ø¯ŲˆØąŲŠ", + "library_scanning": "Ø§Ų„Ų…ØŗØ­ Ø§Ų„Ø¯ŲˆØąŲŠ", + "library_scanning_description": "ØĨؚداد Ų…ØŗØ­ Ø§Ų„Ų…ŲƒØĒب؊ Ø§Ų„Ø¯ŲˆØąŲŠ", + "library_scanning_enable_description": "ØĒŲØšŲŠŲ„ Ų…ØŗØ­ Ø§Ų„Ų…ŲƒØĒب؊ Ø§Ų„Ø¯ŲˆØąŲŠ", "library_settings": "Ø§Ų„Ų…ŲƒØĒب؊ Ø§Ų„ØŽØ§ØąØŦŲŠØŠ", "library_settings_description": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ Ø§Ų„Ų…ŲƒØĒب؊ Ø§Ų„ØŽØ§ØąØŦŲŠØŠ", "library_tasks_description": "Ų…ØŗØ­ Ø§Ų„Ų…ŲƒØĒباØĒ Ø§Ų„ØŽØ§ØąØŦŲŠØŠ Ų„Ų„ØšØĢŲˆØą ØšŲ„Ų‰ Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ ؈/ØŖŲˆ Ø§Ų„Ų…ØĒØēŲŠØąØŠ", - "library_watching_enable_description": "ØąØ§Ų‚Ø¨ Ø§Ų„Ų…ŲƒØĒباØĒ Ø§Ų„ØŽØ§ØąØŦŲŠØŠ Ų„ØĒØĒبؚ ØĒØēŲŠŲŠØąØ§ØĒ Ø§Ų„Ų…Ų„ŲØ§ØĒ", + "library_watching_enable_description": "ØąØ§Ų‚Ø¨ Ø§Ų„Ų…ŲƒØĒباØĒ Ø§Ų„ØŽØ§ØąØŦŲŠØŠ Ų„ØĒØēŲŠŲŠØąØ§ØĒ Ø§Ų„Ų…Ų„ŲØ§ØĒ", "library_watching_settings": "Ų…ØąØ§Ų‚Ø¨ØŠ Ø§Ų„Ų…ŲƒØĒباØĒ (ØĒØŦØąŲŠØ¨ŲŠ)", "library_watching_settings_description": "ØąØ§Ų‚Ø¨ ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§ Ø§Ų„ØĒØēŲŠŲŠØąØ§ØĒ ؁؊ Ø§Ų„Ų…Ų„ŲØ§ØĒ", "logging_enable_description": "ØĒŲØšŲŠŲ„ ØĒØŗØŦŲŠŲ„ Ø§Ų„ØŖØ­Ø¯Ø§ØĢ", "logging_level_description": "ØšŲ†Ø¯ Ø§Ų„ØĒŲØšŲŠŲ„ØŒ ØŖŲŠ Ų…ØŗØĒŲˆŲ‰ ØĒØŗØŦŲŠŲ„ ØŗŲŠØŗØĒØŽØ¯Ų….", - "logging_settings": "ØĒØŗØŦŲŠŲ„ Ø§Ų„ØŖØ­Ø¯Ø§ØĢ", + "logging_settings": "ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø§Ø­Ø¯Ø§ØĢ", "machine_learning_clip_model": "Ų†Ų…ŲˆØ°ØŦ CLIP", "machine_learning_clip_model_description": "Ø§ØŗŲ… Ų†Ų…ŲˆØ°ØŦ CLIP Ų…Ø¯ØąØŦ، Ų‡Ų†Ø§. ŲŠØąØŦŲ‰ Ų…Ų„Ø§Ø­Ø¸ØŠ ØŖŲ†Ų‡ ؊ØŦب ØĨؚاد؊ ØĒØ´ØēŲŠŲ„ ŲˆØ¸ŲŠŲØŠ \"Ø§Ų„Ø¨Ø­ØĢ Ø§Ų„Ø°ŲƒŲŠ\" Ų„ØŦŲ…ŲŠØš Ø§Ų„ØĩŲˆØą بؚد ØĒØēŲŠŲŠØą Ø§Ų„Ų†Ų…ŲˆØ°ØŦ.", "machine_learning_duplicate_detection": "ŲƒØ´Ų Ø§Ų„ØĒŲƒØąØ§Øą", @@ -142,10 +151,10 @@ "map_light_style": "Ø§Ų„Ų†Ų…Øˇ Ø§Ų„ŲØ§ØĒØ­", "map_manage_reverse_geocoding_settings": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ Ø§Ų„ØĒŲƒŲˆŲŠŲ† Ø§Ų„ØŦØēØąØ§ŲŲŠ Ø§Ų„Ų…ØšŲƒŲˆØŗ", "map_reverse_geocoding": "ØšŲƒØŗ Ø§Ų„ØĒØąŲ…ŲŠØ˛ Ø§Ų„ØŦØēØąØ§ŲŲŠ", - "map_reverse_geocoding_enable_description": "ØĒŲØšŲŠŲ„ ØšŲƒØŗ Ø§Ų„ØĒØąŲ…ŲŠØ˛ Ø§Ų„ØŦØēØąØ§ŲŲŠ", - "map_reverse_geocoding_settings": "ØĨؚداداØĒ ØšŲƒØŗ Ø§Ų„ØĒØąŲ…ŲŠØ˛ Ø§Ų„ØŦØēØąØ§ŲŲŠ", - "map_settings": "Ø§Ų„ØŽØąŲŠØˇØŠ", - "map_settings_description": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ Ø§Ų„ØŽØąŲŠØˇØŠ", + "map_reverse_geocoding_enable_description": "ØĒŲØšŲŠŲ„ Ø§Ų„ØĒØąŲ…ŲŠØ˛ Ø§Ų„ØŦØēØąØ§ŲŲŠ Ø§Ų„ØšŲƒØŗŲŠ", + "map_reverse_geocoding_settings": "ØĨؚداداØĒ Ø§Ų„ØĒØąŲ…ŲŠØ˛ Ø§Ų„ØŦØēØąØ§ŲŲŠ Ø§Ų„ØšŲƒØŗŲŠ", + "map_settings": "Ø§Ų„ØŽØ§ØąØˇØŠ", + "map_settings_description": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ Ø§Ų„ØŽØ§ØąØˇØŠ", "map_style_description": "ØšŲ†ŲˆØ§Ų† URL Ų„ØŗŲ…ØŠ Ø§Ų„ØŽØąŲŠØˇØŠ style.json", "memory_cleanup_job": "ØĒŲ†Ø¸ŲŠŲ Ø§Ų„Ø°Ø§ŲƒØąØŠ", "memory_generate_job": "ØĒŲˆŲ„ŲŠØ¯ Ø§Ų„Ø°Ø§ŲƒØąØŠ", @@ -157,12 +166,26 @@ "metadata_settings_description": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ Ø§Ų„ŲˆØĩŲŲŠØŠ", "migration_job": "ØĒØąØ­ŲŠŲ„", "migration_job_description": "ØĒØąØ­ŲŠŲ„ Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĩØēØąØŠ Ų„Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ ŲˆØ§Ų„ŲˆØŦŲˆŲ‡ ØĨŲ„Ų‰ ØŖØ­Ø¯ØĢ Ų‡ŲŠŲƒŲ„ Ų…ØŦŲ„Ø¯Ø§ØĒ", + "nightly_tasks_cluster_faces_setting_description": "Ų‚Ų… بØĒØ´ØēŲŠŲ„ Ø§Ų„ØĒØšØąŲ ØšŲ„Ų‰ Ø§Ų„ŲˆØŦŲ‡ ØšŲ„Ų‰ Ø§Ų„ŲˆØŦŲˆŲ‡ Ø§Ų„Ų…ŲƒØĒØ´ŲØŠ Ø­Ø¯ŲŠØĢا", + "nightly_tasks_cluster_new_faces_setting": "Ų…ØŦŲ…ŲˆØšØŠ Ø§Ų„ŲˆØŦŲˆŲ‡ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ", + "nightly_tasks_database_cleanup_setting": "Ų…Ų‡Ø§Ų… ØĒŲ†Ø¸ŲŠŲ Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ", + "nightly_tasks_database_cleanup_setting_description": "Ų‚Ų… بØĒŲ†Ø¸ŲŠŲ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ Ø§Ų„Ų‚Ø¯ŲŠŲ…ØŠ Ų…Ų†ØĒŲ‡ŲŠØŠ Ø§Ų„ØĩŲ„Ø§Ø­ŲŠØŠ Ų…Ų† Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ", + "nightly_tasks_generate_memories_setting": "ØĨŲ†Ø´Ø§ØĄ Ø§Ų„Ø°ŲƒØąŲŠØ§ØĒ", + "nightly_tasks_generate_memories_setting_description": "ØĨŲ†Ø´Ø§ØĄ Ø°ŲƒØąŲŠØ§ØĒ ØŦØ¯ŲŠØ¯ØŠ Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„", + "nightly_tasks_missing_thumbnails_setting": "ØĨŲ†Ø´Ø§ØĄ ØĩŲˆØą Ų…ØĩØēØąØŠ Ų…ŲŲ‚ŲˆØ¯ØŠ", + "nightly_tasks_missing_thumbnails_setting_description": "ØŖØĩŲˆŲ„ Ų‚Ø§ØĻŲ…ØŠ Ø§Ų„Ø§Ų†ØĒØ¸Ø§Øą Ø¨Ø¯ŲˆŲ† ØĩŲˆØą Ų…ØĩØēØąØŠ Ų„ØĨŲ†Ø´Ø§ØĄ Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĩØēØąØŠ", + "nightly_tasks_settings": "ØĨؚداداØĒ Ø§Ų„Ų…Ų‡Ø§Ų… Ø§Ų„Ų„ŲŠŲ„ŲŠØŠ", + "nightly_tasks_settings_description": "ØĨØ¯Ø§ØąØŠ Ø§Ų„Ų…Ų‡Ø§Ų… Ø§Ų„Ų„ŲŠŲ„ŲŠØŠ", + "nightly_tasks_start_time_setting": "ŲˆŲ‚ØĒ Ø§Ų„Ø¨Ø¯ØĄ", + "nightly_tasks_start_time_setting_description": "Ø§Ų„ŲˆŲ‚ØĒ Ø§Ų„Ø°ŲŠ ŲŠØ¨Ø¯ØŖ ŲŲŠŲ‡ Ø§Ų„ØŽØ§Ø¯Ų… ؁؊ ØĒØ´ØēŲŠŲ„ Ø§Ų„Ų…Ų‡Ø§Ų… Ø§Ų„Ų„ŲŠŲ„ŲŠØŠ", + "nightly_tasks_sync_quota_usage_setting": "Ų…Ø˛Ø§Ų…Ų†ØŠ Ø­ØĩØŠ Ø§Ų„Ø§ØŗØĒØŽØ¯Ø§Ų…", + "nightly_tasks_sync_quota_usage_setting_description": "ØĒØ­Ø¯ŲŠØĢ Ø­ØĩØŠ ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ØŒ Ø¨Ų†Ø§ØĄ ØšŲ„Ų‰ Ø§Ų„Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„Ø­Ø§Ų„ŲŠ", "no_paths_added": "Ų„Ų… ؊ØĒŲ… ØĨØļØ§ŲØŠ ØŖŲŠ Ų…ØŗØ§ØąØ§ØĒ", "no_pattern_added": "Ų„Ų… ؊ØĒŲ… ØĨØļØ§ŲØŠ ØŖŲŠ ØŖŲ†Ų…Ø§Øˇ", - "note_apply_storage_label_previous_assets": "Ų…Ų„Ø§Ø­Ø¸ØŠ: Ų„ØĒØˇØ¨ŲŠŲ‚ ØĒØŗŲ…ŲŠØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØĒŲ… ØąŲØšŲ‡Ø§ ØŗØ§Ø¨Ų‚Ų‹Ø§ØŒ Ų‚Ų… بØĒØ´ØēŲŠŲ„", + "note_apply_storage_label_previous_assets": "Ų…Ų„Ø§Ø­Ø¸ØŠ: Ų„ØĒØˇØ¨ŲŠŲ‚ ØŗŲ…ŲŠØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØĒŲ… ØąŲØšŲ‡Ø§ ØŗØ§Ø¨Ų‚Ų‹Ø§ØŒ Ų‚Ų… بØĒØ´ØēŲŠŲ„", "note_cannot_be_changed_later": "Ų…Ų„Ø§Ø­Ø¸ØŠ: Ų„Ø§ ŲŠŲ…ŲƒŲ† ØĒØēŲŠŲŠØą Ų‡Ø°Ø§ Ų„Ø§Ø­Ų‚Ų‹Ø§!", "notification_email_from_address": "ØšŲ†ŲˆØ§Ų† Ø§Ų„Ų…ØąØŗŲ„", - "notification_email_from_address_description": "ØšŲ†ŲˆØ§Ų† Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ Ų„Ų„Ų…ØąØŗŲ„ØŒ ØšŲ„Ų‰ ØŗØ¨ŲŠŲ„ Ø§Ų„Ų…ØĢØ§Ų„: \"Immich Photo Server noreply@example.com\"", + "notification_email_from_address_description": "ØšŲ†ŲˆØ§Ų† Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ Ų„Ų„Ų…ØąØŗŲ„ØŒ ØšŲ„Ų‰ ØŗØ¨ŲŠŲ„ Ø§Ų„Ų…ØĢØ§Ų„: \"Immich Photo Server noreply@example.com\". ØĒØ§ŲƒØ¯ Ų…Ų† Ø§ØŗØĒØŽØ¯Ø§Ų… ØšŲ†ŲˆØ§Ų† Ø¨ØąŲŠØ¯ Ø§Ų„ŲƒØĒØąŲˆŲ†ŲŠ ŲŠØŗŲ…Ø­ Ų„Ųƒ Ø¨Ø§ØąØŗØ§Ų„ Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„Ø§Ų„ŲƒØĒØąŲˆŲ†ŲŠ Ų…Ų†Ų‡.", "notification_email_host_description": "Ų…Øļ؊؁ ØŽØ§Ø¯Ų… Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ (Ų…ØĢŲ„Ų‹Ø§: smtp.immich.app)", "notification_email_ignore_certificate_errors": "ØĒØŦØ§Ų‡Ų„ ØŖØŽØˇØ§ØĄ Ø§Ų„Ø´Ų‡Ø§Ø¯ØŠ", "notification_email_ignore_certificate_errors_description": "ØĒØŦØ§Ų‡Ų„ ØŖØŽØˇØ§ØĄ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† Øĩح؊ Ø´Ų‡Ø§Ø¯ØŠ TLS (ØēŲŠØą Ų…ØŗØĒØ­ØŗŲ†)", @@ -182,6 +205,7 @@ "oauth_auto_register": "Ø§Ų„ØĒØŗØŦŲŠŲ„ Ø§Ų„ØĒŲ„Ų‚Ø§ØĻ؊", "oauth_auto_register_description": "Ø§Ų„ØĒØŗØŦŲŠŲ„ Ø§Ų„ØĒŲ„Ų‚Ø§ØĻ؊ Ų„Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† Ø§Ų„ØŦدد بؚد ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… OAuth", "oauth_button_text": "Ų†Øĩ Ø§Ų„Ø˛Øą", + "oauth_client_secret_description": "Ų…ØˇŲ„ŲˆØ¨ اذاPKCE(؅؁ØĒاح Ø§Ų„Ø§ØĢباØĒ Ų„ØĒØ¨Ø§Ø¯Ų„ Ø§Ų„ŲƒŲˆØ¯) Ų„Ų… ؊ØĒŲ… ØĒŲˆŲŲŠØąŲ‡ Ų…Ų† Ų…Ø˛ŲˆØ¯ OAuth", "oauth_enable_description": "ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… OAuth", "oauth_mobile_redirect_uri": "ØšŲ†ŲˆØ§Ų† URI Ų„ØĨؚاد؊ Ø§Ų„ØĒ؈ØŦŲŠŲ‡ ØšŲ„Ų‰ Ø§Ų„Ų‡Ø§ØĒ؁", "oauth_mobile_redirect_uri_override": "ØĒØŦØ§ŲˆØ˛ ØšŲ†ŲˆØ§Ų† URI Ų„ØĨؚاد؊ Ø§Ų„ØĒ؈ØŦŲŠŲ‡ ØšŲ„Ų‰ Ø§Ų„Ų‡Ø§ØĒ؁", @@ -189,12 +213,14 @@ "oauth_settings": "OAuth", "oauth_settings_description": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ OAuth", "oauth_settings_more_details": "Ų„Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„ØĒŲØ§ØĩŲŠŲ„ Ø­ŲˆŲ„ Ų‡Ø°Ų‡ Ø§Ų„Ų…ŲŠØ˛ØŠØŒ ŲŠØąØŦŲ‰ Ø§Ų„ØąØŦŲˆØš ØĨŲ„Ų‰ Ø§Ų„ŲˆØĢاØĻŲ‚.", - "oauth_storage_label_claim": "Ø§Ų„Ų…ØˇØ§Ų„Ø¨ØŠ بØĒØĩŲ†ŲŠŲ Ø§Ų„ØĒØŽØ˛ŲŠŲ†", - "oauth_storage_label_claim_description": "Ų‚Ų… ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§ بØĒØšŲŠŲŠŲ† ØĒØĩŲ†ŲŠŲ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„ØŽØ§Øĩ Ø¨Ø§Ų„Ų…ØŗØĒØŽØ¯Ų… ØšŲ„Ų‰ Ų‚ŲŠŲ…ØŠ Ų‡Ø°Ų‡ Ø§Ų„Ų…ØˇØ§Ų„Ø¨ØŠ.", + "oauth_storage_label_claim": "Ø§Ų„Ų…ØˇØ§Ų„Ø¨ØŠ Ø¨ØŗŲ…ØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ†", + "oauth_storage_label_claim_description": "Ų‚Ų… ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§ بØĒØšŲŠŲŠŲ† ØŗŲ…ØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„ØŽØ§Øĩ Ø¨Ø§Ų„Ų…ØŗØĒØŽØ¯Ų… ØšŲ„Ų‰ Ų‚ŲŠŲ…ØŠ Ų‡Ø°Ų‡ Ø§Ų„Ų…ØˇØ§Ų„Ø¨ØŠ.", "oauth_storage_quota_claim": "Ø§Ų„Ų…ØˇØ§Ų„Ø¨ØŠ بحØĩØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ†", "oauth_storage_quota_claim_description": "Ų‚Ų… ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§ بØĒØšŲŠŲŠŲ† Ø­ØĩØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ų„Ų„Ų…ØŗØĒØŽØ¯Ų… ØšŲ„Ų‰ Ų‚ŲŠŲ…ØŠ Ų‡Ø°Ų‡ Ø§Ų„Ų…ØˇØ§Ų„Ø¨ØŠ.", "oauth_storage_quota_default": "Ø­ØĩØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ø§ŲØĒØąØ§ØļŲŠØŠ (ØŦ؊ØŦØ§Ø¨Ø§ŲŠØĒ)", - "oauth_storage_quota_default_description": "Ø§Ų„Ø­ØĩØŠ Ø¨Ø§Ų„ØŦ؊ØŦØ§Ø¨Ø§ŲŠØĒ Ø§Ų„ØĒ؊ ØŗŲŠØĒŲ… Ø§ØŗØĒØŽØ¯Ø§Ų…Ų‡Ø§ ØšŲ†Ø¯Ų…Ø§ Ų„Ø§ ؊ØĒŲ… ØĒŲˆŲŲŠØą Ų…ØˇØ§Ų„Ø¨ØŠ (ØŖØ¯ØŽŲ„ 0 Ų„Ø­ØĩØŠ ØēŲŠØą Ų…Ø­Ø¯ŲˆØ¯ØŠ).", + "oauth_storage_quota_default_description": "Ø§Ų„Ø­ØĩØŠ Ø¨Ø§Ų„ØŦ؊ØŦØ§Ø¨Ø§ŲŠØĒ Ø§Ų„ØĒ؊ ØŗŲŠØĒŲ… Ø§ØŗØĒØŽØ¯Ø§Ų…Ų‡Ø§ ØšŲ†Ø¯Ų…Ø§ Ų„Ø§ ؊ØĒŲ… ØĒŲˆŲŲŠØą Ų…ØˇØ§Ų„Ø¨ØŠ.", + "oauth_timeout": "Ų†ŲØ§Ø° ŲˆŲ‚ØĒ Ø§Ų„ØˇŲ„Ø¨", + "oauth_timeout_description": "Ų†ŲØ§Ø° ŲˆŲ‚ØĒ Ø§Ų„ØˇŲ„Ø¨ Ø¨Ø§Ų„Ų…ŲŠŲ„ŲŠ ØĢØ§Ų†ŲŠØŠ", "password_enable_description": "ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„ŲƒØĒØąŲˆŲ†ŲŠ ŲˆŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", "password_settings": "ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ Ø¨ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", "password_settings_description": "ØĨØ¯Ø§ØąØŠ ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ Ø¨ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", @@ -229,13 +255,14 @@ "storage_template_hash_verification_enabled_description": "ØĒŲØšŲŠŲ„ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„Ų‡Ø§Ø´ØŒ Ų„Ø§ ØĒØšØˇŲ„ Ų‡Ø°Ø§ ØĨŲ„Ø§ ØĨذا ŲƒŲ†ØĒ Ų…ØĒØŖŲƒØ¯Ų‹Ø§ Ų…Ų† ØĒØŖØĢŲŠØąØ§ØĒŲ‡", "storage_template_migration": "ØĒŲ‡ØŦŲŠØą Ų‚Ø§Ų„Ø¨ Ø§Ų„ØĒØŽØ˛ŲŠŲ†", "storage_template_migration_description": "Ų‚Ų… بØĒØˇØ¨ŲŠŲ‚ Ø§Ų„Ų‚Ø§Ų„Ø¨ Ø§Ų„Ø­Ø§Ų„ŲŠ {template} ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØĒŲ… ØąŲØšŲ‡Ø§ ØŗØ§Ø¨Ų‚Ų‹Ø§", - "storage_template_migration_info": "ØĒØēŲŠŲŠØąØ§ØĒ Ø§Ų„Ų‚Ø§Ų„Ø¨ ØŗØĒŲ†ØˇØ¨Ų‚ ŲŲ‚Øˇ ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ. Ų„ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„Ų‚Ø§Ų„Ø¨ ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØĒŲ… ØąŲØšŲ‡Ø§ ØŗØ§Ø¨Ų‚Ų‹Ø§ØŒ Ų‚Ų… بØĒØ´ØēŲŠŲ„ {job}.", + "storage_template_migration_info": "ØĒØēŲŠŲŠØąØ§ØĒ Ø§Ų„Ų†Ų…ŲˆØ°ØŦ Ø§Ų„ØŽØ˛Ų†ŲŠ ØŗØĒØēŲŠØą ØŦŲ…ŲŠØš Ø§Ų„Øĩ؊Øē Ø§Ų„Ų‰ Ø§Ø­ØąŲ ØĩØēŲŠØąØŠ. ØĒØēŲŠŲŠØąØ§ØĒ Ø§Ų„Ų†Ų…ŲˆØ°ØŦ ØŗØĒŲ†ØˇØ¨Ų‚ ŲŲ‚Øˇ ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ. Ų„ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„Ų†Ų…ŲˆØ°ØŦ ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØĒŲ… ØąŲØšŲ‡Ø§ ØŗØ§Ø¨Ų‚Ų‹Ø§ØŒ Ų‚Ų… بØĒØ´ØēŲŠŲ„ {job}.", "storage_template_migration_job": "ŲˆØ¸ŲŠŲØŠ ØĒŲ‡ØŦŲŠØą Ų‚Ø§Ų„Ø¨ Ø§Ų„ØĒØŽØ˛ŲŠŲ†", "storage_template_more_details": "Ų„Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„ØĒŲØ§ØĩŲŠŲ„ Ø­ŲˆŲ„ Ų‡Ø°Ų‡ Ø§Ų„Ų…ŲŠØ˛ØŠØŒ ŲŠØąØŦŲ‰ Ø§Ų„ØąØŦŲˆØš ØĨŲ„Ų‰ Storage Template ؈implications", + "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_user_label": "{label} Ų‡Ųˆ ØŗŲ…ØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", "system_settings": "ØĨؚداداØĒ Ø§Ų„Ų†Ø¸Ø§Ų…", "tag_cleanup_job": "ØĒŲ†Ø¸ŲŠŲ Ø§Ų„ØšŲ„Ø§Ų…ØŠ", "template_email_available_tags": "ŲŠŲ…ŲƒŲ†Ųƒ Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„Ų…ØĒØēŲŠØąØ§ØĒ Ø§Ų„ØĒØ§Ų„ŲŠØŠ ؁؊ Ø§Ų„Ų‚Ø§Ų„Ø¨ Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ: {tags}", @@ -246,15 +273,15 @@ "template_email_update_album": "ØĒØ­Ø¯ŲŠØĢ Ų‚Ø§Ų„Ø¨ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "template_email_welcome": "Ų‚Ø§Ų„Ø¨ Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ Ø§Ų„ØĒØąØ­ŲŠØ¨ŲŠ", "template_settings": "Ų‚ŲˆØ§Ų„Ø¨ Ø§Ų„ØĨØ´ØšØ§ØąØ§ØĒ", - "template_settings_description": "ØĨØ¯Ø§ØąØŠ Ø§Ų„Ų‚ŲˆØ§Ų„Ø¨ Ø§Ų„Ų…ØŽØĩØĩØŠ Ų„Ų„ØĨØ´ØšØ§ØąØ§ØĒ.", + "template_settings_description": "ØĨØ¯Ø§ØąØŠ Ø§Ų„Ų‚ŲˆØ§Ų„Ø¨ Ø§Ų„Ų…ØŽØĩØĩØŠ Ų„Ų„ØĨØ´ØšØ§ØąØ§ØĒ", "theme_custom_css_settings": "CSS Ų…ØŽØĩØĩ", "theme_custom_css_settings_description": "ØŖŲˆØąØ§Ų‚ Ø§Ų„ØŖŲ†Ų…Ø§Øˇ Ø§Ų„Ų…ØĒØĒØ§Ų„ŲŠØŠ ØĒØŗŲ…Ø­ بØĒØŽØĩ؊Øĩ ØĒØĩŲ…ŲŠŲ… Immich.", "theme_settings": "ØĨؚداداØĒ Ø§Ų„ØŗŲ…ØŠ", "theme_settings_description": "ØĨØ¯Ø§ØąØŠ ØĒØŽØĩ؊Øĩ ŲˆØ§ØŦŲ‡ØŠ ŲˆŲŠØ¨ Immich", "thumbnail_generation_job": "ØĨŲ†Ø´Ø§ØĄ Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĩØēØąØŠ", "thumbnail_generation_job_description": "ØĨŲ†Ø´Ø§ØĄ ØĩŲˆØą Ų…ØĩØēØąØŠ ŲƒØ¨ŲŠØąØŠ ؈ØĩØēŲŠØąØŠ ؈ØēŲŠØą ŲˆØ§Øļح؊ Ų„ŲƒŲ„ ØŖØĩŲ„ØŒ Ø¨Ø§Ų„ØĨØļØ§ŲØŠ ØĨŲ„Ų‰ ØĩŲˆØą Ų…ØĩØēØąØŠ Ų„ŲƒŲ„ Ø´ØŽØĩ", - "transcoding_acceleration_api": "ŲˆØ§ØŦŲ‡ØŠ Ø¨ØąŲ…ØŦØŠ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚Ø§ØĒ Ų„Ų„ØĒØŗØąŲŠØš", - "transcoding_acceleration_api_description": "Ø§Ų„ŲˆØ§ØŦŲ‡ØŠ Ø§Ų„Ø¨ØąŲ…ØŦŲŠØŠ Ø§Ų„ØĒ؊ ØŗØĒØĒŲØ§ØšŲ„ Ų…Øš ØŦŲ‡Ø§Ø˛Ųƒ Ų„ØĒØŗØąŲŠØš Ø§Ų„ØĒØ­ŲˆŲŠŲ„. Ų‡Ø°Ø§ Ø§Ų„ØĨؚداد Ų‡Ųˆ \"ØŖŲØļŲ„ Ų…Ø­Ø§ŲˆŲ„ØŠ\": ØŗŲŠØšŲˆØ¯ ØĨŲ„Ų‰ Ø§Ų„ØĒØ­ŲˆŲŠŲ„ Ø§Ų„Ø¨ØąŲ…ØŦ؊ ؁؊ Ø­Ø§Ų„ØŠ Ø§Ų„ŲØ´Ų„. Ų‚Ø¯ Ų„Ø§ ŲŠØšŲ…Ų„ VP9 اؚØĒŲ…Ø§Ø¯Ų‹Ø§ ØšŲ„Ų‰ ØšØĒØ§Ø¯Ųƒ.", + "transcoding_acceleration_api": "ØĒØŗØąŲŠØš API", + "transcoding_acceleration_api_description": "API Ø§Ų„ØĒ؊ ØŗØĒØĒŲØ§ØšŲ„ Ų…Øš ØŦŲ‡Ø§Ø˛Ųƒ Ų„ØĒØŗØąŲŠØš Ø§Ų„ØĒØ­ŲˆŲŠŲ„. Ų‡Ø°Ø§ Ø§Ų„ØĨؚداد Ų‡Ųˆ \"ØŖŲØļŲ„ Ų…Ø­Ø§ŲˆŲ„ØŠ\": ØŗŲŠØšŲˆØ¯ ØĨŲ„Ų‰ Ø§Ų„ØĒØ­ŲˆŲŠŲ„ Ø§Ų„Ø¨ØąŲ…ØŦ؊ ؁؊ Ø­Ø§Ų„ØŠ Ø§Ų„ŲØ´Ų„. Ų‚Ø¯ Ų„Ø§ ŲŠØšŲ…Ų„ VP9 اؚØĒŲ…Ø§Ø¯Ų‹Ø§ ØšŲ„Ų‰ ØšØĒØ§Ø¯Ųƒ.", "transcoding_acceleration_nvenc": "NVENC (؊ØĒØˇŲ„Ø¨ GPU Ų…Ų† NVIDIA)", "transcoding_acceleration_qsv": "Quick Sync (؊ØĒØˇŲ„Ø¨ Ų…ØšØ§Ų„ØŦ Intel Ų…Ų† Ø§Ų„ØŦŲŠŲ„ Ø§Ų„ØŗØ§Ø¨Øš ØŖŲˆ ØŖØ­Ø¯ØĢ)", "transcoding_acceleration_rkmpp": "RKMPP (ŲŲ‚Øˇ ØšŲ„Ų‰ Ø´ØąØ§ØĻØ­ Rockchip SOC)", @@ -278,13 +305,13 @@ "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": "ŲŠŲ†ØˇØ¨Ų‚ Ø°Ų„Ųƒ ŲŲ‚Øˇ ØšŲ„Ų‰ NVENC، QSV، ؈ RKMPP. ŲŠŲ…ŲƒŲ† Ø§Ų„ØĒØŗØąŲŠØš Ų…Ų† ØˇØąŲ Ų„ØˇØąŲ Ø¨Ø¯Ų„Ø§Ų‹ Ų…Ų† ØĒØŗØąŲŠØš Ø§Ų„ØĒØąŲ…ŲŠØ˛ ŲŲ‚Øˇ. Ų‚Ø¯ Ų„Ø§ ŲŠØšŲ…Ų„ ØšŲ„Ų‰ ØŦŲ…ŲŠØš Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ.", "transcoding_max_b_frames": "ØŖŲ‚ØĩŲ‰ ؚدد Ų…Ų† Ø§Ų„ØĨØˇØ§ØąØ§ØĒ B", "transcoding_max_b_frames_description": "Ø§Ų„Ų‚ŲŠŲ… Ø§Ų„ØŖØšŲ„Ų‰ ØĒØšØ˛Ø˛ ŲƒŲØ§ØĄØŠ Ø§Ų„ØļØēØˇØŒ ŲˆŲ„ŲƒŲ†Ų‡Ø§ ØĒØ¨ØˇØĻ ØšŲ…Ų„ŲŠØŠ Ø§Ų„ØĒØąŲ…ŲŠØ˛. Ų‚Ø¯ Ų„Ø§ ØĒŲƒŲˆŲ† Ų…ØĒŲˆØ§ŲŲ‚ØŠ Ų…Øš Ø§Ų„ØĒØŗØąŲŠØš Ø§Ų„ØšØĒØ§Ø¯ŲŠ ØšŲ„Ų‰ Ø§Ų„ØŖØŦŲ‡Ø˛ØŠ Ø§Ų„Ų‚Ø¯ŲŠŲ…ØŠ. Ų‚ŲŠŲ…ØŠ 0 ØĒØšØˇŲ„ ØĨØˇØ§ØąØ§ØĒ B، Ø¨ŲŠŲ†Ų…Ø§ ØĒØļØ¨Øˇ Ø§Ų„Ų‚ŲŠŲ…ØŠ -1 Ų‡Ø°Ø§ Ø§Ų„Ų‚ŲŠŲ…ØŠ ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§.", "transcoding_max_bitrate": "Ø§Ų„Ø­Ø¯ Ø§Ų„ØŖŲ‚ØĩŲ‰ Ų„Ų…ØšØ¯Ų„ Ø§Ų„Ø¨ØĒ", - "transcoding_max_bitrate_description": "ŲŠŲ…ŲƒŲ† ØŖŲ† ŲŠØ¤Ø¯ŲŠ ØĒØšŲŠŲŠŲ† Ø§Ų„Ø­Ø¯ Ø§Ų„ØŖŲ‚ØĩŲ‰ Ų„Ų…ØšØ¯Ų„ Ø§Ų„Ø¨ØĒ ØĨŲ„Ų‰ ØŦØšŲ„ ØŖØ­ØŦØ§Ų… Ø§Ų„Ų…Ų„ŲØ§ØĒ ØŖŲƒØĢØą Ų‚Ø§Ø¨Ų„ŲŠØŠ Ų„Ų„ØĒŲ†Ø¨Ø¤ Ø¨Ų‡Ø§ بØĒŲƒŲ„ŲØŠ Ø¨ØŗŲŠØˇØŠ Ø¨Ø§Ų„Ų†ØŗØ¨ØŠ Ų„Ų„ØŦŲˆØ¯ØŠ. ØšŲ†Ø¯ Ø¯Ų‚ØŠ 720 Ø¨ŲƒØŗŲ„ØŒ ØĒŲƒŲˆŲ† Ø§Ų„Ų‚ŲŠŲ… Ø§Ų„Ų†Ų…ŲˆØ°ØŦŲŠØŠ 2600 ŲƒŲŠŲ„Ųˆ Ø¨Ø§ŲŠØĒ Ų„Ų€ VP9 ØŖŲˆ HEVC، ØŖŲˆ 4500 ŲƒŲŠŲ„Ųˆ Ø¨Ø§ŲŠØĒ Ų„Ų€ H.264. Ų…ØšØˇŲ„ ØĨذا ØĒŲ… ØļØ¨ØˇŲ‡ ØšŲ„Ų‰ 0.", + "transcoding_max_bitrate_description": "ŲŠŲ…ŲƒŲ† ØŖŲ† ŲŠØ¤Ø¯ŲŠ ØĒØšŲŠŲŠŲ† Ø§Ų„Ø­Ø¯ Ø§Ų„ØŖŲ‚ØĩŲ‰ Ų„Ų…ØšØ¯Ų„ Ø§Ų„Ø¨ØĒ ØĨŲ„Ų‰ ØŦØšŲ„ ØŖØ­ØŦØ§Ų… Ø§Ų„Ų…Ų„ŲØ§ØĒ ØŖŲƒØĢØą Ų‚Ø§Ø¨Ų„ŲŠØŠ Ų„Ų„ØĒŲ†Ø¨Ø¤ Ø¨Ų‡Ø§ بØĒŲƒŲ„ŲØŠ Ø¨ØŗŲŠØˇØŠ Ø¨Ø§Ų„Ų†ØŗØ¨ØŠ Ų„Ų„ØŦŲˆØ¯ØŠ. ØšŲ†Ø¯ Ø¯Ų‚ØŠ 720 Ø¨ŲƒØŗŲ„ØŒ ØĒŲƒŲˆŲ† Ø§Ų„Ų‚ŲŠŲ… Ø§Ų„Ų†Ų…ŲˆØ°ØŦŲŠØŠ 2600 ŲƒŲŠŲ„Ųˆ بØĒ Ų„Ų€ VP9 ØŖŲˆ HEVC، ØŖŲˆ 4500 ŲƒŲŠŲ„Ųˆ بØĒ Ų„Ų€ H.264. Ų…ØšØˇŲ„ ØĨذا ØĒŲ… ØļØ¨ØˇŲ‡ ØšŲ„Ų‰ 0.", "transcoding_max_keyframe_interval": "Ø§Ų„Ø­Ø¯ Ø§Ų„ØŖŲ‚ØĩŲ‰ Ų„Ų„ŲØ§ØĩŲ„ Ø§Ų„Ø˛Ų…Ų†ŲŠ Ų„Ų„ØĨØˇØ§Øą Ø§Ų„ØąØĻŲŠØŗŲŠ", "transcoding_max_keyframe_interval_description": "؊ØļØ¨Øˇ Ø§Ų„Ø­Ø¯ Ø§Ų„ØŖŲ‚ØĩŲ‰ Ų„Ų…ØŗØ§ŲØŠ Ø§Ų„ØĨØˇØ§Øą Ø¨ŲŠŲ† Ø§Ų„ØĨØˇØ§ØąØ§ØĒ Ø§Ų„ØąØĻŲŠØŗŲŠØŠ. ØĒØ¤Ø¯ŲŠ Ø§Ų„Ų‚ŲŠŲ… Ø§Ų„Ų…Ų†ØŽŲØļØŠ ØĨŲ„Ų‰ Ø˛ŲŠØ§Ø¯ØŠ ØŗŲˆØĄ ŲƒŲØ§ØĄØŠ Ø§Ų„ØļØēØˇØŒ ŲˆŲ„ŲƒŲ†Ų‡Ø§ ØĒØšŲ…Ų„ ØšŲ„Ų‰ ØĒØ­ØŗŲŠŲ† ØŖŲˆŲ‚Ø§ØĒ Ø§Ų„Ø¨Ø­ØĢ ŲˆŲ‚Ø¯ ØĒØšŲ…Ų„ ØšŲ„Ų‰ ØĒØ­ØŗŲŠŲ† Ø§Ų„ØŦŲˆØ¯ØŠ ؁؊ Ø§Ų„Ų…Ø´Ø§Ų‡Ø¯ ذاØĒ Ø§Ų„Ø­ØąŲƒØŠ Ø§Ų„ØŗØąŲŠØšØŠ. 0 ؊ØļØ¨Øˇ Ų‡Ø°Ų‡ Ø§Ų„Ų‚ŲŠŲ…ØŠ ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§.", "transcoding_optimal_description": "Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ ذاØĒ Ø§Ų„Ø¯Ų‚ØŠ Ø§Ų„ØŖØšŲ„Ų‰ Ų…Ų† Ø§Ų„Ø¯Ų‚ØŠ Ø§Ų„Ų…ØŗØĒŲ‡Ø¯ŲØŠ ØŖŲˆ بØĒŲ†ØŗŲŠŲ‚ ØēŲŠØą Ų…Ų‚Ø¨ŲˆŲ„", @@ -324,6 +351,7 @@ "user_delete_delay_settings_description": "ؚدد Ø§Ų„ØŖŲŠØ§Ų… بؚد Ø§Ų„ØĨØ˛Ø§Ų„ØŠ Ų„Ø­Ø°Ų Ø­ØŗØ§Ø¨ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų… ŲˆŲ…Ø­ØĒŲˆŲŠØ§ØĒŲ‡ Ø¨Ø´ŲƒŲ„ داØĻŲ…. ØĒŲ‚ŲˆŲ… ŲˆØ¸ŲŠŲØŠ Ø­Ø°Ų Ø§Ų„Ų…ØŗØĒØŽØ¯Ų… Ø¨Ø§Ų„ØĒØ´ØēŲŠŲ„ ؁؊ Ų…Ų†ØĒØĩ؁ Ø§Ų„Ų„ŲŠŲ„ Ų„Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† Ø§Ų„ØŦØ§Ų‡Ø˛ŲŠŲ† Ų„Ų„Ø­Ø°Ų. ØŗŲŠØĒŲ… ØĒŲ‚ŲŠŲŠŲ… Ø§Ų„ØĒØēŲŠŲŠØąØ§ØĒ ØšŲ„Ų‰ Ų‡Ø°Ø§ Ø§Ų„ØĨؚداد ؁؊ Ø§Ų„ØĒŲ†ŲŲŠØ° Ø§Ų„Ų‚Ø§Ø¯Ų….", "user_delete_immediately": "ØŗŲŠØĒŲ… ؈ØļØš Ø­ØŗØ§Ø¨ {user} ŲˆŲ…Ø­ØĒŲˆŲŠØ§ØĒŲ‡ ؁؊ Ų‚Ø§ØĻŲ…ØŠ Ø§Ų„Ø§Ų†ØĒØ¸Ø§Øą Ų„Ų„Ø­Ø°Ų Ø§Ų„Ø¯Ø§ØĻŲ… ØšŲ„Ų‰ Ø§Ų„ŲŲˆØą.", "user_delete_immediately_checkbox": "Ų‚Ø§ØĻŲ…ØŠ Ø§Ų†ØĒØ¸Ø§Øą Ø§Ų„Ų…ØŗØĒØŽØ¯Ų… ŲˆØ§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ų„Ų„Ø­Ø°Ų Ø§Ų„ŲŲˆØąŲŠ", + "user_details": "ØĒŲØ§ØĩŲŠŲ„ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", "user_management": "ØĨØ¯Ø§ØąØŠ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", "user_password_has_been_reset": "ØĒŲ…ØĒ ØĨؚاد؊ ØĒØšŲŠŲŠŲ† ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…:", "user_password_reset_description": "ŲŠØąØŦŲ‰ ØĒØ˛ŲˆŲŠØ¯ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų… Ø¨ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą Ø§Ų„Ų…Ø¤Ų‚ØĒØŠ ؈ØĨØ¨Ų„Ø§ØēŲ‡ Ø¨ØŖŲ†Ų‡ ØŗŲŠØ­ØĒاØŦ ØĨŲ„Ų‰ ØĒØēŲŠŲŠØą ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą ØšŲ†Ø¯ ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ Ø§Ų„ØĒØ§Ų„ŲŠ.", @@ -343,9 +371,17 @@ "admin_password": "ŲƒŲ„Ų…ØŠ ØŗØą Ø§Ų„Ų…Ø´ØąŲ", "administration": "Ø§Ų„ØĨØ¯Ø§ØąØŠ", "advanced": "Ų…ØĒŲ‚Ø¯Ų…", + "advanced_settings_enable_alternate_media_filter_subtitle": "Ø§ØŗØĒØŽØ¯Ų… Ų‡Ø°Ø§ Ø§Ų„ØŽŲŠØ§Øą Ų„ØĒØĩŲŲŠØŠ Ø§Ų„ŲˆØŗØ§ØĻØˇ اØĢŲ†Ø§ØĄ Ø§Ų„Ų…Ø˛Ø§Ų…Ų†Ų‡ Ø¨Ų†Ø§ØĄ ØšŲ„Ų‰ Ų…ØšØ§ŲŠŲŠØą Ø¨Ø¯ŲŠŲ„ØŠ. ØŦØąØ¨ Ų‡Ø°Ø§ Ø§Ų„ØŽŲŠØ§Øą ŲŲ‚Øˇ ŲƒØ§Ų† Ų„Ø¯ŲŠŲƒ Ų…Ø´Ø§ŲƒŲ„ Ų…Øš Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ Ø¨Ø§Ų„ŲƒØ´Ų ØšŲ† ØŦŲ…ŲŠØš Ø§Ų„Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ.", + "advanced_settings_enable_alternate_media_filter_title": "[ØĒØŦØąŲŠØ¨ŲŠ] Ø§ØŗØĒØŽØ¯Ų… ØŦŲ‡Ø§Ø˛ ØĒØĩŲŲŠØŠ Ų…Ø˛Ø§Ų…Ų†Ų‡ Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ Ø¨Ø¯ŲŠŲ„", + "advanced_settings_log_level_title": "Ų…ØŗØĒŲˆŲ‰ Ø§Ų„ØŗØŦŲ„: {level}", "advanced_settings_prefer_remote_subtitle": "ØĒŲƒŲˆŲ† بؚØļ Ø§Ų„ØŖØŦŲ‡Ø˛ØŠ Ø¨ØˇŲŠØĻØŠ Ų„Ų„ØēØ§ŲŠØŠ ؁؊ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĩØēØąØŠ Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„Ų…ŲˆØŦŲˆØ¯ØŠ ØšŲ„Ų‰ Ø§Ų„ØŦŲ‡Ø§Ø˛. Ų‚Ų… بØĒŲ†Ø´ŲŠØˇ Ų‡Ø°Ø§ Ø§Ų„ØĨؚداد Ų„ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØĩŲˆØą Ø§Ų„Ø¨ØšŲŠØ¯ØŠ Ø¨Ø¯Ų„Ø§Ų‹ Ų…Ų† Ø°Ų„Ųƒ.", "advanced_settings_prefer_remote_title": "ØĒ؁ØļŲ„ Ø§Ų„ØĩŲˆØą Ø§Ų„Ø¨ØšŲŠØ¯ØŠ", + "advanced_settings_proxy_headers_subtitle": "ØšØąŲ ØšŲ†Ø§ŲˆŲŠŲ† Ø§Ų„ŲˆŲƒŲŠŲ„ Ø§Ų„ØĒ؊ ŲŠØŗØĒØŽØ¯Ų…Ų‡Ø§ Immich Ų„Ø§ØąØŗØ§Ų„ ŲƒŲ„ ØˇŲ„Ø¨ Ø´Ø¨ŲƒŲŠ", + "advanced_settings_proxy_headers_title": "ØšŲ†Ø§ŲˆŲŠŲ† Ø§Ų„ŲˆŲƒŲŠŲ„", + "advanced_settings_self_signed_ssl_subtitle": "ØĒØŽØˇŲŠ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† Ø´Ų‡Ø§Ø¯ØŠ SSL Ų„ØŽØ§Ø¯Ų… Ø§Ų„Ų†Ų‚ØˇØŠ Ø§Ų„Ų†Ų‡Ø§ØĻ؊. Ų…ŲƒŲ„ŲˆØ¨ Ų„Ų„Ø´Ų‡Ø§Ø¯Ø§ØĒ Ø§Ų„Ų…ŲˆŲ‚ØšØŠ ذاØĒŲŠØ§.", "advanced_settings_self_signed_ssl_title": "Ø§Ų„ØŗŲ…Ø§Ø­ Ø¨Ø´Ų‡Ø§Ø¯Ø§ØĒ SSL Ø§Ų„Ų…ŲˆŲ‚ØšØŠ ذاØĒŲŠŲ‹Ø§", + "advanced_settings_sync_remote_deletions_subtitle": "Ø­Ø°Ų Ø§Ųˆ Ø§ØŗØĒؚاد؊ ØĒŲ„Ų‚Ø§ØĻ؊ Ų„Ų„Ø§ØĩŲˆŲ„ ØšŲ„Ų‰ Ų‡Ø°Ø§ Ø§Ų„ØŦŲ‡Ø§Ø˛ ØšŲ†Ø¯ ØĒŲ†ŲŲŠØ° Ø§Ų„ØšŲ…Ų„ŲŠØŠ ØšŲ„Ų‰ Ø§Ų„ŲˆŲŠØ¨", + "advanced_settings_sync_remote_deletions_title": "Ų…Ø˛Ø§Ų…Ų†ØŠ ØšŲ…Ų„ŲŠØ§ØĒ Ø§Ų„Ø­Ø°Ų ØšŲ† بؚد [ØĒØŦØąŲŠØ¨ŲŠ]", "advanced_settings_tile_subtitle": "ØĨؚداداØĒ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų… Ø§Ų„Ų…ØĒŲ‚Ø¯Ų…ØŠ", "advanced_settings_troubleshooting_subtitle": "ØĒŲ…ŲƒŲŠŲ† Ø§Ų„Ų…ŲŠØ˛Ø§ØĒ Ø§Ų„ØĨØļØ§ŲŲŠØŠ Ų„Ø§ØŗØĒŲƒØ´Ø§Ų Ø§Ų„ØŖØŽØˇØ§ØĄ ؈ØĨØĩŲ„Ø§Ø­Ų‡Ø§", "advanced_settings_troubleshooting_title": "Ø§ØŗØĒŲƒØ´Ø§Ų Ø§Ų„ØŖØŽØˇØ§ØĄ ؈ØĨØĩŲ„Ø§Ø­Ų‡Ø§", @@ -382,6 +418,9 @@ "album_with_link_access": "Ø§Ų„ØŗŲ…Ø§Ø­ Ų„ØŖŲŠ Ø´ØŽØĩ Ų„Ø¯ŲŠŲ‡ Ø§Ų„ØąØ§Ø¨Øˇ Ø¨ØąØ¤ŲŠØŠ Ø§Ų„ØĩŲˆØą ŲˆØ§Ų„ØŖØ´ØŽØ§Øĩ Ø§Ų„Ų…ŲˆØŦŲˆØ¯ŲŠŲ† ؁؊ Ų‡Ø°Ø§ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ….", "albums": "Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ", "albums_count": "{count, plural, one {{count, number} ØŖŲ„Ø¨ŲˆŲ…} other {{count, number} ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ}}", + "albums_default_sort_order": "ØĒØąØĒŲŠØ¨ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ… Ø§Ų„Ø§ŲØĒØąØ§Øļ؊", + "albums_default_sort_order_description": "ØĒØąØĒŲŠØ¨ ŲØąØ˛ Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŖŲˆŲ„ŲŠ ØšŲ†Ø¯ ØĨŲ†Ø´Ø§ØĄ ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ ØŦØ¯ŲŠØ¯ØŠ.", + "albums_feature_description": "Ų…ØŦŲ…ŲˆØšØŠ Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØĒ؊ ŲŠŲ…ŲƒŲ† Ų…Ø´Ø§ØąŲƒØĒŲ‡Ø§ Ų…Øš Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† ØĸØŽØąŲŠŲ†.", "all": "Ø§Ų„ŲƒŲ„", "all_albums": "ØŦŲ…ŲŠØš Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ", "all_people": "ØŦŲ…ŲŠØš Ø§Ų„ØŖØ´ØŽØ§Øĩ", @@ -392,20 +431,23 @@ "allow_public_user_to_upload": "Ø§Ų„ØŗŲ…Ø§Ø­ Ų„Ų„Ų…ØŗØĒØŽØ¯Ų… Ø§Ų„ØšØ§Ų… Ø¨Ø§Ų„ØąŲØš", "alt_text_qr_code": "ØĩŲˆØąØŠ ØąŲ…Ø˛ Ø§Ų„Ø§ØŗØĒØŦاب؊ Ø§Ų„ØŗØąŲŠØšØŠ (QR)", "anti_clockwise": "ØšŲƒØŗ اØĒØŦØ§Ų‡ ØšŲ‚Ø§ØąØ¨ Ø§Ų„ØŗØ§ØšØŠ", - "api_key": "؅؁ØĒاح ŲˆØ§ØŦŲ‡ØŠ Ø¨ØąŲ…ØŦØŠ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚Ø§ØĒ", + "api_key": "؅؁ØĒاح API", "api_key_description": "ØŗŲŠØĒŲ… ØšØąØļ Ų‡Ø°Ų‡ Ø§Ų„Ų‚ŲŠŲ…ØŠ Ų…ØąØŠ ŲˆØ§Ø­Ø¯ØŠ ŲŲ‚Øˇ. ŲŠØąØŦŲ‰ Ø§Ų„ØĒØŖŲƒØ¯ Ų…Ų† Ų†ØŗØŽŲ‡Ø§ Ų‚Ø¨Ų„ ØĨØēŲ„Ø§Ų‚ Ø§Ų„Ų†Ø§ŲØ°ØŠ.", "api_key_empty": "؊ØŦب ØŖŲ„Ø§ ŲŠŲƒŲˆŲ† Ø§ØŗŲ… ؅؁ØĒاح API ŲØ§ØąØēŲ‹Ø§", - "api_keys": "Ų…ŲØ§ØĒŲŠØ­ ŲˆØ§ØŦŲ‡ØŠ Ø¨ØąŲ…ØŦØŠ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚Ø§ØĒ", + "api_keys": "Ų…ŲØ§ØĒŲŠØ­ API", "app_bar_signout_dialog_content": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ Ø§Ų„ØŽØąŲˆØŦ", "app_bar_signout_dialog_ok": "Ų†ØšŲ…", "app_bar_signout_dialog_title": "ØŽØąŲˆØŦ", "app_settings": "ØĨؚداداØĒ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚", "appears_in": "ŲŠØ¸Ų‡Øą ؁؊", "archive": "Ø§Ų„ØŖØąØ´ŲŠŲ", + "archive_action_prompt": "{count} اØļ؊؁ ØĨŲ„Ų‰ Ø§Ų„Ø§ØąØ´ŲŠŲ", "archive_or_unarchive_photo": "ØŖØąØ´ŲØŠ Ø§Ų„ØĩŲˆØąØŠ ØŖŲˆ ØĨŲ„ØēØ§ØĄ ØŖØąØ´ŲØĒŲ‡Ø§", "archive_page_no_archived_assets": "Ų„Ų… ؊ØĒŲ… Ø§Ų„ØšØĢŲˆØą ØšŲ„Ų‰ Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„Ų…Ø¤ØąØ´ŲØŠ", + "archive_page_title": "Ø§ØąØ´ŲŠŲ ({count})", "archive_size": "Ø­ØŦŲ… Ø§Ų„ØŖØąØ´ŲŠŲ", "archive_size_description": "ØĒŲƒŲˆŲŠŲ† Ø­ØŦŲ… Ø§Ų„ØŖØąØ´ŲŠŲ Ų„Ų„ØĒŲ†Ø˛ŲŠŲ„Ø§ØĒ (Ø¨Ø§Ų„ØŦ؊ØŦØ§Ø¨Ø§ŲŠØĒ)", + "archived": "Ų…Ø¤ØąØ´ŲØŠ", "archived_count": "{count, plural, other {Ø§Ų„ØŖØąØ´ŲŠŲ #}}", "are_these_the_same_person": "Ų‡Ų„ Ų‡Ø¤Ų„Ø§ØĄ Ų‡Ų… Ų†ŲØŗ Ø§Ų„Ø´ØŽØĩ؟", "are_you_sure_to_do_this": "Ų‡Ų„ Ø§Ų†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† ØŖŲ†Ųƒ ØĒØąŲŠØ¯ ØŖŲ† ØĒŲØšŲ„ Ų‡Ø°Ø§ØŸ", @@ -427,37 +469,55 @@ "asset_list_settings_title": "Ø´Ø¨ŲƒØŠ Ø§Ų„ØĩŲˆØą", "asset_offline": "Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ ØēŲŠØą اØĒØĩØ§Ų„", "asset_offline_description": "Ų„Ų… ŲŠØšØ¯ Ų‡Ø°Ø§ Ø§Ų„ØŖØĩŲ„ Ø§Ų„ØŽØ§ØąØŦ؊ Ų…ŲˆØŦŲˆØ¯Ų‹Ø§ ØšŲ„Ų‰ Ø§Ų„Ų‚ØąØĩ. ŲŠØąØŦŲ‰ Ø§Ų„Ø§ØĒØĩØ§Ų„ Ø¨Ų…ØŗØ¤ŲˆŲ„ Immich Ų„Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ Ø§Ų„Ų…ØŗØ§ØšØ¯ØŠ.", + "asset_restored_successfully": "ØĒŲ… Ø§ØŗØĒؚاد؊ Ø§Ų„Ø§ØĩŲ„ Ø¨Ų†ØŦاح", "asset_skipped": "ØĒŲ… ØĒØŽØˇŲŠŲ‡", "asset_skipped_in_trash": "؁؊ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "asset_uploaded": "ØĒŲ… Ø§Ų„ØąŲØš", "asset_uploading": "ØŦØ§ØąŲ Ø§Ų„ØąŲØšâ€Ļ", + "asset_viewer_settings_subtitle": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ ØšØ§ØąØļ Ø§Ų„Ų…ØšØąØļ Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ", "asset_viewer_settings_title": "ØšØ§ØąØļ Ø§Ų„ØŖØĩŲˆŲ„", "assets": "Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ", "assets_added_count": "ØĒŲ…ØĒ ØĨØļØ§ŲØŠ {count, plural, one {# Ų…Ø­ØĒŲˆŲ‰} other {# Ų…Ø­ØĒŲˆŲŠØ§ØĒ}}", "assets_added_to_album_count": "ØĒŲ…ØĒ ØĨØļØ§ŲØŠ {count, plural, one {# Ø§Ų„ØŖØĩŲ„} other {# Ø§Ų„ØŖØĩŲˆŲ„}} ØĨŲ„Ų‰ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", - "assets_added_to_name_count": "ØĒŲ… ØĨØļØ§ŲØŠ {count, plural, one {# Ų…Ø­ØĒŲˆŲ‰} other {# Ų…Ø­ØĒŲˆŲŠØ§ØĒ }} ØĨŲ„Ų‰ {hasName, select, true {{name}} other {ØŖŲ„Ø¨ŲˆŲ… ØŦØ¯ŲŠØ¯}}", + "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} Ų„Ø§ŲŠŲ…ŲƒŲ† اØļØ§ŲØĒŲ‡ Ø§Ų„Ų‰ Ø§Ų„Ø§Ų„Ø¨ŲˆŲ…", "assets_count": "{count, plural, one {# Ų…Ø­ØĒŲˆŲ‰} other {# Ų…Ø­ØĒŲˆŲŠØ§ØĒ}}", + "assets_deleted_permanently": "{count} Ø§Ų„Ø§Øĩ(؈)Ų„ Ø§Ų„Ų…Ø­Ø°ŲˆŲ(Ų‡) Ø¨Ø´ŲƒŲ„ داØĻŲ…", + "assets_deleted_permanently_from_server": "{count} Ø§Ų„Ø§Øĩ(؈)Ų„ Ø§Ų„Ų…Ø­Ø°ŲˆŲ(Ų‡) Ø¨Ø´ŲƒŲ„ داØĻŲ…ŲŠ Ų…Ų† ØŽØ§Ø¯Ų… Immich", + "assets_downloaded_failed": "{count, plural, one {ØĒŲ… Ø§Ų„ØĒØ­Ų…ŲŠŲ„ # ؅؄؁ - {error} ؅؄؁ ŲØ´Ų„} other {ØĒŲ… Ø§Ų„ØĒØ­Ų…ŲŠŲ„ # Ų…Ų„ŲØ§ØĒ - {error} Ų…Ų„ŲØ§ØĒ ŲØ´Ų„ØĒ}}", + "assets_downloaded_successfully": "{count, plural, one {ØĒŲ… Ø§Ų„ØĒØ­Ų…ŲŠŲ„ # ؅؄؁ Ø¨Ų†ØŦاح} other {ØĒŲ… Ø§Ų„ØĒØ­Ų…ŲŠŲ„ # Ų…Ų„ŲØ§ØĒ Ø¨Ų†ØŦاح}}", "assets_moved_to_trash_count": "ØĒŲ… Ų†Ų‚Ų„ {count, plural, one {# Ų…Ø­ØĒŲˆŲ‰} 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": "{count} Ø§Ų„Ø§Øĩ(؈)Ų„ Ø§Ų„Ų…Ų†Ų‚ŲˆŲ„ØŠ Ø§Ų„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ Ų…Ų† ØŽØ§Ø¯Ų… Immich", "assets_were_part_of_album_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_location_permission": "Ø§Ø°Ų† Ø§Ų„ŲˆØĩŲˆŲ„ Ų„Ų„Ų…ŲˆŲ‚Øš ؁؊ Ø§Ų„ØŽŲ„ŲŲŠØŠ", + "background_location_permission_content": "Ų„Ų„ØĒŲ…ŲƒŲ† Ų…Ų† ØĒØ¨Ø¯ŲŠŲ„ Ø§Ų„Ø´Ø¨ŲƒŲ‡ Ø¨Ø§Ų„ØŽŲ„ŲŲŠØŠØŒ Immich ŲŠØ­ØĒاØŦ*داØĻŲ…Ø§* Ų„Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ Ų…ŲˆŲ‚Øš Ø¯Ų‚ŲŠŲ‚ Ų„ŲŠØĒŲ…ŲƒŲ† Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ Ų…Ų† Ų‚ØąØ§ØĻØŠ Ø§ØŗŲ… Ø´Ø¨ŲƒØŠ Ø§Ų„Wi-Fi", + "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_all": "Ø§Ų„ØŦŲ…ŲŠØš", - "backup_background_service_backup_failed_message": "ŲØ´Ų„ ؁؊ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų„ØŖØĩŲˆŲ„. ØŦØ§ØąŲ ØĨؚاد؊ Ø§Ų„Ų…Ø­Ø§ŲˆŲ„ØŠ...", - "backup_background_service_connection_failed_message": "ŲØ´Ų„ ؁؊ Ø§Ų„Ø§ØĒØĩØ§Ų„ Ø¨Ø§Ų„ØŽØ§Ø¯Ų…. ØŦØ§ØąŲ ØĨؚاد؊ Ø§Ų„Ų…Ø­Ø§ŲˆŲ„ØŠ...", - "backup_background_service_default_notification": "Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ ...", + "backup_background_service_backup_failed_message": "ŲØ´Ų„ ؁؊ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų„ØŖØĩŲˆŲ„. ØŦØ§ØąŲ ØĨؚاد؊ Ø§Ų„Ų…Ø­Ø§ŲˆŲ„ØŠâ€Ļ", + "backup_background_service_connection_failed_message": "ŲØ´Ų„ ؁؊ Ø§Ų„Ø§ØĒØĩØ§Ų„ Ø¨Ø§Ų„ØŽØ§Ø¯Ų…. ØŦØ§ØąŲ ØĨؚاد؊ Ø§Ų„Ų…Ø­Ø§ŲˆŲ„ØŠâ€Ļ", + "backup_background_service_current_upload_notification": "ØĒØ­Ų…ŲŠŲ„ {filename}", + "backup_background_service_default_notification": "Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠâ€Ļ", "backup_background_service_error_title": "ØŽØˇØŖ ؁؊ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", - "backup_background_service_in_progress_notification": "Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ...", + "backup_background_service_in_progress_notification": "Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒâ€Ļ", + "backup_background_service_upload_failure_notification": "ŲØ´Ų„ ØĒØ­Ų…ŲŠŲ„ {filename}", "backup_controller_page_albums": "ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ احØĒŲŠØ§ØˇŲŠØŠ", "backup_controller_page_background_app_refresh_disabled_content": "Ų‚Ų… بØĒŲ…ŲƒŲŠŲ† ØĒØ­Ø¯ŲŠØĢ ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„ØŽŲ„ŲŲŠØŠ ؁؊ Ø§Ų„ØĨؚداداØĒ > ØšØ§Ų… > ØĒØ­Ø¯ŲŠØĢ ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„ØŽŲ„ŲŲŠØŠ Ų„Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ ؁؊ Ø§Ų„ØŽŲ„ŲŲŠØŠ.", "backup_controller_page_background_app_refresh_disabled_title": "ØĒŲ… ØĒØšØˇŲŠŲ„ ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ ؁؊ Ø§Ų„ØŽŲ„ŲŲŠØŠ", @@ -468,17 +528,22 @@ "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_background_wifi": "ŲŲ‚Øˇ ØšŲ„Ų‰ Wi-Fi", "backup_controller_page_backup": "Ø¯ØšŲ…", "backup_controller_page_backup_selected": "Ø§Ų„Ų…Ø­Ø¯Ø¯: ", "backup_controller_page_backup_sub": "Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ", + "backup_controller_page_created": "Ø§Ų†Ø´ØĻ ؁؊ :{date}", "backup_controller_page_desc_backup": "Ų‚Ų… بØĒØ´ØēŲŠŲ„ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ø§Ų„ØŖŲ…Ø§Ų…ŲŠ Ų„ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§ ØĨŲ„Ų‰ Ø§Ų„ØŽØ§Ø¯Ų… ØšŲ†Ø¯ ؁ØĒØ­ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚.", "backup_controller_page_excluded": "Ų…ØŗØĒبؚد: ", + "backup_controller_page_failed": "ŲØ´Ų„ ({count})", + "backup_controller_page_filename": "Ø§ØŗŲ… Ø§Ų„Ų…Ų„Ų : {filename} [{size}]", + "backup_controller_page_id": "Ų‡ŲˆŲŠØŠ: {id}", "backup_controller_page_info": "Ų…ØšŲ„ŲˆŲ…Ø§ØĒ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", "backup_controller_page_none_selected": "Ų„Ų… ؊ØĒŲ… Ø§Ų„ØĒØ­Ø¯ŲŠØ¯", "backup_controller_page_remainder": "Ø¨Ų‚ŲŠØŠ", @@ -487,7 +552,8 @@ "backup_controller_page_start_backup": "Ø¨Ø¯ØĄ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", "backup_controller_page_status_off": "Ø§Ų„Ų†ØŗØŽØŠ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠØŠ Ø§Ų„ØĒŲ„Ų‚Ø§ØĻŲŠØŠ ØēŲŠØą ŲØšØ§Ų„ØŠ", "backup_controller_page_status_on": "Ø§Ų„Ų†ØŗØŽØŠ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠØŠ Ø§Ų„ØĒŲ„Ų‚Ø§ØĻŲŠØŠ ŲØšØ§Ų„ØŠ", - "backup_controller_page_to_backup": "Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠØŠ", + "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": "Ų‚Ų… بØĒØ´ØēŲŠŲ„ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ø§Ų„Ų…Ų‚Ø¯Ų…ØŠ", @@ -499,7 +565,12 @@ "backup_manual_success": "Ų†ØŦاح", "backup_manual_title": "Ø­Ø§Ų„ØŠ Ø§Ų„ØĒØ­Ų…ŲŠŲ„", "backup_options_page_title": "ØŽŲŠØ§ØąØ§ØĒ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", + "backup_setting_subtitle": "Ø§Ø¯Ø§ØąØŠ اؚداداØĒ Ø§Ų„ØĒØ­Ų…ŲŠŲ„ ؁؊ Ø§Ų„ØŽŲ„ŲŲŠØŠ ŲˆØ§Ų„Ų…Ų‚Ø¯Ų…ØŠ", "backward": "Ø§Ų„Ų‰ Ø§Ų„ŲˆØąØ§ØĄ", + "biometric_auth_enabled": "Ø§Ų„Ų…ØĩØ§Ø¯Ų‚ØŠ Ø§Ų„Ø¨Ø§ŲŠŲˆŲ…ØĒØąŲŠØŠ Ų…ŲØšŲ„Ų‡", + "biometric_locked_out": "Ų„Ų‚Ø¯ ؂؁؄ØĒ ØšŲ†Ųƒ Ø§Ų„Ų…ØĩØ§Ø¯Ų‚ØŠ Ø§Ų„Ø¨ŲŠŲˆŲ…ØĒØąŲŠØŠ", + "biometric_no_options": "Ų„Ø§ ØĒ؈ØŦد ØŽŲŠØ§ØąØ§ØĒ Ø¨Ø§ŲŠŲˆŲ…ØĒØąŲŠØŠ Ų…ØĒŲˆŲØąØŠ", + "biometric_not_available": "Ø§Ø§Ų„Ų…ØĩØ§Ø¯Ų‚ØŠ Ø§Ų„Ø¨ŲŠŲˆŲ…ØĒØąŲŠØŠ ØēŲŠØą Ų…ØĒاح؊ ØšŲ„Ų‰ Ų‡Ø°Ø§ Ø§Ų„ØŦŲ‡Ø§Ø˛", "birthdate_saved": "ØĒŲ… Ø­ŲØ¸ ØĒØ§ØąŲŠØŽ Ø§Ų„Ų…ŲŠŲ„Ø§Ø¯ Ø¨Ų†ØŦاح", "birthdate_set_description": "؊ØĒŲ… Ø§ØŗØĒØŽØ¯Ø§Ų… ØĒØ§ØąŲŠØŽ Ø§Ų„Ų…ŲŠŲ„Ø§Ø¯ Ų„Ø­ØŗØ§Ø¨ ØšŲ…Øą Ų‡Ø°Ø§ Ø§Ų„Ø´ØŽØĩ ŲˆŲ‚ØĒ Ø§Ų„ØĒŲ‚Ø§Øˇ Ø§Ų„ØĩŲˆØąØŠ.", "blurred_background": "ØŽŲ„ŲŲŠØŠ Ų…Ø´ŲˆØ´ØŠ", @@ -514,12 +585,13 @@ "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_subtitle": "ØĒØ­ŲƒŲ… ؁؊ ØŗŲ„ŲˆŲƒ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø¤Ų‚ØĒ Ų„ØĒØˇØ¨ŲŠŲ‚ Immich Ø§Ų„ØŦŲˆØ§Ų„", "cache_settings_tile_subtitle": "Ø§Ų„ØĒØ­ŲƒŲ… ؁؊ ØŗŲ„ŲˆŲƒ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø­Ų„ŲŠ", "cache_settings_tile_title": "Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø­Ų„ŲŠ", "cache_settings_title": "ØĨؚداداØĒ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø¤Ų‚ØĒ", @@ -527,11 +599,16 @@ "camera_brand": "ØšŲ„Ø§Ų…ØŠ Ø§Ų„ŲƒØ§Ų…ŲŠØąØ§ Ø§Ų„ØĒØŦØ§ØąŲŠØŠ", "camera_model": "ØˇØąØ§Ø˛ Ø§Ų„ŲƒØ§Ų…ŲŠØąØ§", "cancel": "ØĨŲ„ØēØ§ØĄ", - "cancel_search": "Ø§Ų„Øē؊ Ø§Ų„Ø¨Ø­ØĢ", + "cancel_search": "Ø§Ų„ØēØ§ØĄ Ø§Ų„Ø¨Ø­ØĢ", + "canceled": "ØĒŲ… Ø§Ų„Ø§Ų„ØēØ§ØĄ", "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": "ØĒØēŲŠŲŠØą Ø§Ų„ØĨØŗŲ…", @@ -543,9 +620,12 @@ "change_password_form_new_password": "ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ", "change_password_form_password_mismatch": "ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą ØēŲŠØą Ų…ØˇØ§Ø¨Ų‚ØŠ", "change_password_form_reenter_new_password": "ØŖØšØ¯ ØĨØ¯ØŽØ§Ų„ ŲƒŲ„Ų…ØŠ Ų…ØąŲˆØą ØŦØ¯ŲŠØ¯ØŠ", - "change_pin_code": "ØĒØēŲŠŲŠØą Ø§Ų„ØąŲ‚Ų… Ø§Ų„ØŗØąŲŠ", + "change_pin_code": "ØĒØēŲŠŲŠØą ØąŲ…Ø˛ PIN", "change_your_password": "ØēŲŠØą ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ", "changed_visibility_successfully": "ØĒŲ… ØĒØēŲŠŲŠØą Ø§Ų„ØąØ¤ŲŠØŠ Ø¨Ų†ØŦاح", + "check_corrupt_asset_backup": "Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† ؈ØŦŲˆØ¯ Ų†ØŗØŽ احØĒŲŠØ§ØˇŲŠØŠ ŲØ§ØŗØ¯ØŠ Ų„Ų„Ø§ØĩŲˆŲ„", + "check_corrupt_asset_backup_button": "اØŦØąØ§ØĄ ŲØ­Øĩ", + "check_corrupt_asset_backup_description": "Ų‚Ų… بØĨØŦØąØ§ØĄ Ų‡Ø°Ø§ Ø§Ų„ŲØ­Øĩ ŲŲ‚Øˇ ØšØ¨Øą Ø´Ø¨ŲƒØŠ Wi-Fi ŲˆØ¨ØšØ¯ Ų†ØŗØŽ ØŦŲ…ŲŠØš Ø§Ų„ØŖØĩŲˆŲ„ احØĒŲŠØ§ØˇŲŠŲ‹Ø§. Ų‚Ø¯ ŲŠØŗØĒØēØąŲ‚ Ø§Ų„ØĨØŦØąØ§ØĄ بØļØš Ø¯Ų‚Ø§ØĻŲ‚.", "check_logs": "ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„ØŗØŦŲ„Ø§ØĒ", "choose_matching_people_to_merge": "ا؎ØĒØą Ø§Ų„ØŖØ´ØŽØ§Øĩ Ø§Ų„Ų…ØĒØˇØ§Ø¨Ų‚ŲŠŲ† Ų„Ø¯Ų…ØŦŲ‡Ų…", "city": "Ø§Ų„Ų…Ø¯ŲŠŲ†ØŠ", @@ -554,6 +634,14 @@ "clear_all_recent_searches": "Ų…ØŗØ­ ØŦŲ…ŲŠØš ØšŲ…Ų„ŲŠØ§ØĒ Ø§Ų„Ø¨Ø­ØĢ Ø§Ų„ØŖØŽŲŠØąØŠ", "clear_message": "ØĨØŽŲ„Ø§ØĄ Ø§Ų„ØąØŗØ§Ų„ØŠ", "clear_value": "ØĨØŽŲ„Ø§ØĄ Ø§Ų„Ų‚ŲŠŲ…ØŠ", + "client_cert_dialog_msg_confirm": "Ø­ØŗŲ†Ø§", + "client_cert_enter_password": "Ø§Ø¯ØŽŲ„ ŲƒŲ„Ų…ØŠ ØŗØą", + "client_cert_import": "Ø§ØŗØĒŲŠØąØ§Ø¯", + "client_cert_import_success_msg": "ØĒŲ… Ø§ØŗØĒŲŠØąØ§Ø¯ Ø´Ų‡Ø§Ø¯ØŠ Ø§Ų„ØšŲ…ŲŠŲ„", + "client_cert_invalid_msg": "؅؄؁ Ø´Ų‡Ø§Ø¯ØŠ ØšŲ…ŲŠŲ„ ØēŲŠØą ØĩØ§Ų„Ø­ØŠ Ø§Ųˆ ŲƒŲ„Ų…ØŠ ØŗØą ØēŲŠØą ØĩØ­ŲŠØ­ØŠ", + "client_cert_remove_msg": "ØĒŲ… Ø§Ø˛Ø§Ų„ØŠ Ø´Ų‡Ø§Ø¯ØŠ Ø§Ų„ØšŲ…ŲŠŲ„", + "client_cert_subtitle": "ŲŠØ¯ØšŲ… Øĩ؊Øē PKCS12 (.p12, .pfx)ŲŲ‚Øˇ. Ø§ØŗØĒŲŠØąØ§Ø¯/Ø§Ø˛Ø§Ų„ØŠ Ø§Ų„Ø´Ų‡Ø§Ø¯Ø§ØĒ Ų…ØĒاح ŲŲ‚Øˇ Ų‚Ø¨Ų„ ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„", + "client_cert_title": "Ø´Ų‡Ø§Ø¯ØŠ Ų…ØŗØĒØŽØ¯Ų… SSL", "clockwise": "باØĒØŦØ§Ų‡ ØšŲ‚Ø§ØąØ¨ Ø§Ų„ØŗØ§ØšØŠ", "close": "ØĨØēŲ„Ø§Ų‚", "collapse": "ØˇŲŠ", @@ -566,21 +654,27 @@ "comments_are_disabled": "Ø§Ų„ØĒØšŲ„ŲŠŲ‚Ø§ØĒ Ų…ØšØˇŲ„ØŠ", "common_create_new_album": "ØĨŲ†Ø´Ø§ØĄ ØŖŲ„Ø¨ŲˆŲ… ØŦØ¯ŲŠØ¯", "common_server_error": "ŲŠØąØŦŲ‰ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† اØĒØĩØ§Ų„ Ø§Ų„Ø´Ø¨ŲƒØŠ Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ ، ŲˆØ§Ų„ØĒØŖŲƒØ¯ Ų…Ų† ØŖŲ† Ø§Ų„ØŦŲ‡Ø§Ø˛ Ų‚Ø§Ø¨Ų„ Ų„Ų„ŲˆØĩŲˆŲ„ ؈ØĨØĩØ¯Ø§ØąØ§ØĒ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚/Ø§Ų„ØŦŲ‡Ø§Ø˛ Ų…ØĒŲˆØ§ŲŲ‚ØŠ.", + "completed": "Ø§ŲƒØĒŲ…Ų„", "confirm": "ØĒØŖŲƒŲŠØ¯", "confirm_admin_password": "ØĒØŖŲƒŲŠØ¯ ŲƒŲ„Ų…ØŠ Ų…ØąŲˆØą Ø§Ų„Ų…ØŗØ¤ŲˆŲ„", "confirm_delete_face": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† Ø­Ø°Ų ؈ØŦŲ‡ {name} Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„ØŸ", "confirm_delete_shared_link": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ Ø­Ø°Ų Ų‡Ø°Ø§ Ø§Ų„ØąØ§Ø¨Øˇ Ø§Ų„Ų…Ø´ØĒØąŲƒØŸ", "confirm_keep_this_delete_others": "ØŗŲŠØĒŲ… Ø­Ø°Ų ØŦŲ…ŲŠØš Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŖØŽØąŲ‰ ؁؊ Ø§Ų„Ų…ØŦŲ…ŲˆØšØŠ Ø¨Ø§ØŗØĒØĢŲ†Ø§ØĄ Ų‡Ø°Ø§ Ø§Ų„ØŖØĩŲ„. Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† ØŖŲ†Ųƒ ØĒØąŲŠØ¯ Ø§Ų„Ų…ØĒابؚ؊؟", - "confirm_new_pin_code": "ØĢبØĒ Ø§Ų„ØąŲ‚Ų… Ø§Ų„ØŗØąŲŠ Ø§Ų„ØŦØ¯ŲŠØ¯", + "confirm_new_pin_code": "ØĢبØĒ ØąŲ…Ø˛ PIN Ø§Ų„ØŦØ¯ŲŠØ¯", "confirm_password": "ØĒØŖŲƒŲŠØ¯ ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", + "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": " Ø­Ø°Ų Ų…Ų†Ø§Ų„ ØĒØˇØ¨ŲŠŲ‚", + "control_bottom_app_bar_delete_from_immich": "Ø­Ø°Ų Ų…Ų† Immich", "control_bottom_app_bar_delete_from_local": "Ø­Ø°Ų Ų…Ų† Ø§Ų„ØŦŲ‡Ø§Ø˛", "control_bottom_app_bar_edit_location": "ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ŲˆØŦŲ‡ØŠ", "control_bottom_app_bar_edit_time": "ØĒØ­ØąŲŠØą Ø§Ų„ØĒØ§ØąŲŠØŽ ŲˆØ§Ų„ŲˆŲ‚ØĒ", + "control_bottom_app_bar_share_link": "Ų…Ø´Ø§ØąŲƒØŠ ØąØ§Ø¨Øˇ", "control_bottom_app_bar_share_to": "Ų…Ø´Ø§ØąŲƒØŠ ØĨŲ„Ų‰", "control_bottom_app_bar_trash_from_immich": "Ø­Ø°ŲŲ‡ ŲˆŲ†Ų‚Ų„Ų‡ ؁؊ ØŗŲ„Ų‡ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "copied_image_to_clipboard": "ØĒŲ… Ų†ØŗØŽ Ø§Ų„ØĩŲˆØąØŠ ØĨŲ„Ų‰ Ø§Ų„Ø­Ø§ŲØ¸ØŠ.", @@ -602,6 +696,7 @@ "create_link": "ØĨŲ†Ø´Ø§ØĄ ØąØ§Ø¨Øˇ", "create_link_to_share": "ØĨŲ†Ø´Ø§ØĄ ØąØ§Ø¨Øˇ Ų„Ų„Ų…Ø´Ø§ØąŲƒØŠ", "create_link_to_share_description": "Ø§Ų„ØŗŲ…Ø§Ø­ Ų„ØŖŲŠ Ø´ØŽØĩ Ų„Ø¯ŲŠŲ‡ Ø§Ų„ØąØ§Ø¨Øˇ Ø¨Ų…Ø´Ø§Ų‡Ø¯ØŠ Ø§Ų„ØĩŲˆØąØŠ (Ø§Ų„ØĩŲˆØą) Ø§Ų„Ų…Ø­Ø¯Ø¯ØŠ", + "create_new": "Ø§Ų†Ø´Ø§ØĄ ØŦØ¯ŲŠØ¯", "create_new_person": "ØĨŲ†Ø´Ø§ØĄ Ø´ØŽØĩ ØŦØ¯ŲŠØ¯", "create_new_person_hint": "ØĒØšŲŠŲŠŲ† Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„Ų…Ø­Ø¯Ø¯ØŠ Ų„Ø´ØŽØĩ ØŦØ¯ŲŠØ¯", "create_new_user": "ØĨŲ†Ø´Ø§ØĄ Ų…ØŗØĒØŽØ¯Ų… ØŦØ¯ŲŠØ¯", @@ -611,14 +706,18 @@ "create_tag_description": "ØŖŲ†Ø´ØĻ ØšŲ„Ø§Ų…ØŠ ØŦØ¯ŲŠØ¯ØŠ. Ø¨Ø§Ų„Ų†ØŗØ¨ØŠ Ų„Ų„ØšŲ„Ø§Ų…Ø§ØĒ Ø§Ų„Ų…ØĒØ¯Ø§ØŽŲ„ØŠØŒ ŲŠØąØŦŲ‰ ØĨØ¯ØŽØ§Ų„ Ø§Ų„Ų…ØŗØ§Øą Ø§Ų„ŲƒØ§Ų…Ų„ Ų„Ų„ØšŲ„Ø§Ų…ØŠ Ø¨Ų…Ø§ ؁؊ Ø°Ų„Ųƒ Ø§Ų„ØŽØˇŲˆØˇ Ø§Ų„Ų…Ø§ØĻŲ„ØŠ Ų„Ų„ØŖŲ…Ø§Ų….", "create_user": "ØĨŲ†Ø´Ø§ØĄ Ų…ØŗØĒØŽØ¯Ų…", "created": "ØĒŲ… Ø§Ų„ØĨŲ†Ø´Ø§ØĄ", + "created_at": "Ų…ØŽŲ„ŲˆŲ‚", + "crop": "Ų‚Øĩ", "curated_object_page_title": "ØŖØ´ŲŠØ§ØĄ", "current_device": "Ø§Ų„ØŦŲ‡Ø§Ø˛ Ø§Ų„Ø­Ø§Ų„ŲŠ", - "current_pin_code": "Ø§Ų„ØąŲ‚Ų… Ø§Ų„ØŗØąŲŠ Ø§Ų„Ø­Ø§Ų„ŲŠ", + "current_pin_code": "ØąŲ…Ø˛ PIN Ø§Ų„Ø­Ø§Ų„ŲŠ", + "current_server_address": "ØšŲ†ŲˆØ§Ų† Ø§Ų„ØŽØ§Ø¯Ų… Ø§Ų„Ø­Ø§Ų„ŲŠ", "custom_locale": "Ų„ØēØŠ Ų…ØŽØĩØĩØŠ", "custom_locale_description": "ØĒŲ†ØŗŲŠŲ‚ Ø§Ų„ØĒŲˆØ§ØąŲŠØŽ ŲˆØ§Ų„ØŖØąŲ‚Ø§Ų… Ø¨Ų†Ø§ØĄŲ‹ ØšŲ„Ų‰ Ø§Ų„Ų„ØēØŠ ŲˆØ§Ų„Ų…Ų†ØˇŲ‚ØŠ", "daily_title_text_date": "E ، MMM DD", "daily_title_text_date_year": "E ، MMM DD ، yyyy", "dark": "Ų…ØšØĒŲ…", + "dark_theme": "ØĒØ¨Ø¯ŲŠŲ„ Ø§Ų„Ų…Ø¸Ų‡Øą Ø§Ų„Ø¯Ø§ŲƒŲ†", "date_after": "Ø§Ų„ØĒØ§ØąØŽ بؚد", "date_and_time": "Ø§Ų„ØĒØ§ØąŲŠØŽ ؈ Ø§Ų„ŲˆŲ‚ØĒ", "date_before": "Ø§Ų„ØĒØ§ØąŲŠØŽ Ų‚Ø¨Ų„", @@ -634,11 +733,12 @@ "default_locale": "Ø§Ų„Ų„ØēØŠ Ø§Ų„Ø§ŲØĒØąØ§ØļŲŠØŠ", "default_locale_description": "ØĒŲ†ØŗŲŠŲ‚ Ø§Ų„ØĒŲˆØ§ØąŲŠØŽ ŲˆØ§Ų„ØŖØąŲ‚Ø§Ų… Ø¨Ų†Ø§ØĄŲ‹ ØšŲ„Ų‰ Ų„ØēØŠ Ø§Ų„Ų…ØĒØĩŲØ­ Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ", "delete": "Ø­Ø°Ų", + "delete_action_prompt": "{count} Ø­Ø°Ų Ø¨Ø´ŲƒŲ„ Ų†Ų‡Ø§ØĻ؊", "delete_album": "Ø­Ø°Ų Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "delete_api_key_prompt": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ Ø­Ø°Ų ؅؁ØĒاح API Ų‡Ø°Ø§ØŸ", - "delete_dialog_alert": " Ų‡Ø°Ų‡ Ø§Ų„ØšŲ†Ø§ØĩØą ØŗŲŠØĒŲ… Ø­Ø°ŲŲ‡Ø§ Ø¨Ø´ŲƒŲ„ داØĻŲ… Ų…Ų† ØŦŲ‡Ø§Ø˛Ųƒ ŲˆŲ…Ų† ØĒØˇØ¨ŲŠŲ‚", - "delete_dialog_alert_local": " Ø§Ų„ØšŲ†Ø§ØĩØą Ø§Ų„ØĒ؊ ØĒŲ… Ø­Ø°ŲŲ‡Ø§ Ų…Ų† ØŦŲ‡Ø§Ø˛Ųƒ ŲˆŲ„ŲƒŲ†Ų‡Ø§ Ų…ŲˆØŦŲˆØ¯Ų‡ ؁؊ ØĒØˇØ¨ŲŠŲ‚", - "delete_dialog_alert_local_non_backed_up": "بؚØļ Ø§Ų„ØšŲ†Ø§ØĩØą Ø§Ų„ØĒ؊ ØŗŲŠØĒŲ… Ø­Ø°ŲŲ‡Ø§ Ø¨Ø´ŲƒŲ„ داØĻŲ… ŲˆŲ„Ø§ ؊؈ØŦد Ų„Ų‡Ø§ Ų†ØŗØŽŲ‡ احØĒŲŠØ§ØˇŲŠŲ‡ ؁؊ ØĒØˇØ¨ŲŠŲ‚ ", + "delete_dialog_alert": "Ų‡Ø°Ų‡ Ø§Ų„ØšŲ†Ø§ØĩØą ØŗŲŠØĒŲ… Ø­Ø°ŲŲ‡Ø§ Ø¨Ø´ŲƒŲ„ داØĻŲ… Ų…Ų† Immich ؈ Ų…Ų† ØŦŲ‡Ø§Ø˛Ųƒ", + "delete_dialog_alert_local": "Ø§Ų„ØšŲ†Ø§ØĩØą Ø§Ų„ØĒ؊ ØŗŲŠØĒŲ… Ø­Ø°ŲŲ‡Ø§ Ų…Ų† ØŦŲ‡Ø§Ø˛Ųƒ ŲˆŲ„ŲƒŲ† ØĒØ¨Ų‚Ų‰ Ų…ŲˆØŦŲˆØ¯Ų‡ ؁؊ ØŽØ§Ø¯Ų… Immich", + "delete_dialog_alert_local_non_backed_up": "بؚØļ Ø§Ų„ØšŲ†Ø§ØĩØą ØēŲŠØą Ų…Ø¯ØšŲˆŲ…ØŠ Ø¨Ų†ØŗØŽØŠ احØĒŲŠØ§ØˇŲŠØŠ ØšŲ„Ų‰ Immich ŲˆØŗŲŠØĒŲ… ØĨØ˛Ø§Ų„ØĒŲ‡Ø§ Ų†Ų‡Ø§ØĻŲŠŲ‹Ø§ Ų…Ų† ØŦŲ‡Ø§Ø˛Ųƒ", "delete_dialog_alert_remote": "Ø§Ų„ØšŲ†Ø§ØĩØą Ø§Ų„ØĒ؊ ØŗŲŠØĒŲ… Ø­Ø°ŲŲ‡Ø§ Ø¨Ø´ŲƒŲ„ داØĻŲ… Ų…Ų† ØĒØˇØ¨ŲŠŲ‚", "delete_dialog_ok_force": "Ø§Ø­Ø°Ų ØšŲ„Ų‰ ØŖŲŠ Ø­Ø§Ų„", "delete_dialog_title": "Ø§Ų„Ø­Ø°Ų Ø¨Ø´ŲƒŲ„ Ų†Ų‡Ø§ØĻ؊", @@ -664,7 +764,9 @@ "direction": "Ø§Ų„ØĨØĒØŦØ§Ų‡", "disabled": "Ų…ØšØˇŲ„", "disallow_edits": "Ų…Ų†Øš Ø§Ų„ØĒØšØ¯ŲŠŲ„Ø§ØĒ", + "discord": "Ø¯ØŗŲƒŲˆØąØ¯", "discover": "Ø§ŲƒØĒØ´Ų", + "discovered_devices": "اØŦŲ‡Ø˛ØŠ Ų…ŲƒØĒØ´ŲØŠ", "dismiss_all_errors": "ØĒØŦØ§Ų‡Ų„ ŲƒØ§ŲØŠ Ø§Ų„ØŖØŽØˇØ§ØĄ", "dismiss_error": "ØĒØŦØ§Ų‡Ų„ Ø§Ų„ØŽØˇØŖ", "display_options": "ØšØąØļ Ø§Ų„ØŽŲŠØ§ØąØ§ØĒ", @@ -675,12 +777,25 @@ "documentation": "Ø§Ų„ŲˆØĢاØĻŲ‚", "done": "ØĒŲ…", "download": "ØĒŲ†Ø˛ŲŠŲ„", + "download_canceled": "Ø§Ų„Øē؊ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", + "download_complete": "Ø§ŲƒØĒŲ…Ų„ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", + "download_enqueue": "ØĒŲ†Ø˛ŲŠŲ„ ؁؊ Ų‚Ø§ØĻŲ…ØŠ Ø§Ų„Ø§Ų†ØĒØ¸Ø§Øą", + "download_error": "ØŽØˇØ§ ؁؊ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", + "download_failed": "ŲØ´Ų„ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", + "download_finished": "Ø§Ų†ØĒŲ‡Ų‰ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", "download_include_embedded_motion_videos": "Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…Ø¯Ų…ØŦØŠ", "download_include_embedded_motion_videos_description": "ØĒØļŲ…ŲŠŲ† Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…ØļŲ…Ų†ØŠ ؁؊ Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĒØ­ØąŲƒØŠ ŲƒŲ…Ų„Ų ؅؆؁ØĩŲ„", + "download_notfound": "Ų„Ų… ŲŠØšØĢØą ØšŲ„Ų‰ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", + "download_paused": "Ø§ŲˆŲ‚Ų Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", "download_settings": "Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„Ø§ØĒ", "download_settings_description": "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØĨؚداداØĒ Ø§Ų„Ų…ØĒØšŲ„Ų‚ØŠ بØĒŲ†Ø˛ŲŠŲ„ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ", + "download_started": "بدا Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", + "download_sucess": "Ų†ØŦØ­ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", + "download_sucess_android": "ØĒŲ… ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ŲˆØŗØ§ØĻØˇ Ø§Ų„Ų‰ DCIM/Immich", + "download_waiting_to_retry": "Ø§Ų„Ø§Ų†ØĒØ¸Ø§Øą Ų„Ų„Ų…Ø­Ø§ŲˆŲ„ØŠ", "downloading": "ØŦØ§ØąŲ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", "downloading_asset_filename": "{filename} Ų‚ŲŠØ¯ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", + "downloading_media": "ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ŲˆØŗØ§ØĻØˇ", "drop_files_to_upload": "Ų‚Ų… بØĨØŗŲ‚Ø§Øˇ Ø§Ų„Ų…Ų„ŲØ§ØĒ ؁؊ ØŖŲŠ Ų…ŲƒØ§Ų† Ų„ØąŲØšŲ‡Ø§", "duplicates": "Ø§Ų„ØĒŲƒØąØ§ØąØ§ØĒ", "duplicates_description": "Ų‚Ų… Ø¨Ø­Ų„ ŲƒŲ„ Ų…ØŦŲ…ŲˆØšØŠ Ų…Ų† ØŽŲ„Ø§Ų„ Ø§Ų„ØĨØ´Ø§ØąØŠ ØĨŲ„Ų‰ Ø§Ų„ØĒŲƒØąØ§ØąØ§ØĒ، ØĨŲ† ؈ØŦدØĒ", @@ -690,6 +805,8 @@ "edit_avatar": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„Ø´ØŽØĩŲŠØŠ", "edit_date": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„ØĒØ§ØąŲŠØŽ", "edit_date_and_time": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„ØĒØ§ØąŲŠØŽ ŲˆØ§Ų„ŲˆŲ‚ØĒ", + "edit_description": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„ŲˆØĩ؁", + "edit_description_prompt": "Ø§Ų„ØąØŦØ§ØĄ ا؎ØĒŲŠØ§Øą ؈Øĩ؁ ØŦØ¯ŲŠØ¯:", "edit_exclusion_pattern": "ØĒØšØ¯ŲŠŲ„ Ų†Ų…Øˇ Ø§Ų„Ø§ØŗØĒبؚاد", "edit_faces": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„ŲˆØŦŲˆŲ‡", "edit_import_path": "ØĒØšØ¯ŲŠŲ„ Ų…ØŗØ§Øą Ø§Ų„Ø§ØŗØĒŲŠØąØ§Ø¯", @@ -697,6 +814,7 @@ "edit_key": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„Ų…ŲØĒاح", "edit_link": "ØĒØēŲŠŲŠØą Ø§Ų„ØąØ§Ø¨Øˇ", "edit_location": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„Ų…ŲˆŲ‚Øš", + "edit_location_action_prompt": "{count} Ų…ŲˆŲ‚Øš ØĒŲ… ØĒØšØ¯ŲŠŲ„Ų‡", "edit_location_dialog_title": "Ų…ŲˆŲ‚Øš", "edit_name": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„Ø§ØŗŲ…", "edit_people": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„ØŖØ´ØŽØ§Øĩ", @@ -710,15 +828,24 @@ "editor_crop_tool_h2_aspect_ratios": "Ų†ØŗØ¨ Ø§Ų„ØšØąØļ ØĨŲ„Ų‰ Ø§Ų„Ø§ØąØĒŲØ§Øš", "editor_crop_tool_h2_rotation": "Ø§Ų„ØĒØ¯ŲˆŲŠØą", "email": "Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ", + "email_notifications": "ØĒŲ†Ø¨ŲŠŲ‡Ø§ØĒ Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„Ø§Ų„ŲƒØĒØąŲˆŲ†ŲŠ", + "empty_folder": "Ų‡Ø°Ø§ Ø§Ų„Ų…ØŦŲ„Ø¯ ŲØ§ØąØē", "empty_trash": "ØŖŲØąØē ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "empty_trash_confirmation": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ ØĨŲØąØ§Øē ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ؟ ØŗŲŠØ¤Ø¯ŲŠ Ų‡Ø°Ø§ ØĨŲ„Ų‰ ØĨØ˛Ø§Ų„ØŠ ØŦŲ…ŲŠØš Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„Ų…ŲˆØŦŲˆØ¯ØŠ ؁؊ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ Ø¨Ø´ŲƒŲ„ Ų†Ų‡Ø§ØĻ؊ Ų…Ų† Immich.\nŲ„Ø§ ŲŠŲ…ŲƒŲ†Ųƒ Ø§Ų„ØĒØąØ§ØŦØš ØšŲ† Ų‡Ø°Ø§ Ø§Ų„ØĨØŦØąØ§ØĄ!", "enable": "ØĒŲØšŲŠŲ„", + "enable_biometric_auth_description": "ØŖØ¯ØŽŲ„ ØąŲ…Ø˛ PIN Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ Ų„ØĒŲ…ŲƒŲŠŲ† Ø§Ų„Ų…ØĩØ§Ø¯Ų‚ØŠ Ø§Ų„Ø¨ŲŠŲˆŲ…ØĒØąŲŠØŠ", "enabled": "Ų…ŲØšŲ„", "end_date": "ØĒØ§ØąŲŠØŽ Ø§Ų„ØĨŲ†ØĒŲ‡Ø§ØĄ", - "enter_wifi_name": "Enter WiFi name", + "enqueued": "Ų…ŲØ¯ØąØŦ ؁؊ Ø§Ų„ØˇØ§Ø¨ŲˆØą", + "enter_wifi_name": "Ø§Ø¯ØŽŲ„ Ø§ØŗŲ… Wi-Fi", + "enter_your_pin_code": "ØŖØ¯ØŽŲ„ ØąŲ…Ø˛ PIN Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ", + "enter_your_pin_code_subtitle": "ØŖØ¯ØŽŲ„ ØąŲ…Ø˛ PIN Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ Ų„Ų„ŲˆØĩŲˆŲ„ ØĨŲ„Ų‰ Ø§Ų„Ų…ØŦŲ„Ø¯ Ø§Ų„Ų…Ų‚ŲŲ„", "error": "ØŽØˇØŖ", + "error_change_sort_album": "ŲØ´Ų„ ؁؊ ØĒØēŲŠŲŠØą ØĒØąØĒŲŠØ¨ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "error_delete_face": "حدØĢ ØŽØˇØŖ ؁؊ Ø­Ø°Ų Ø§Ų„ŲˆØŦŲ‡ Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„", "error_loading_image": "حدØĢ ØŽØˇØŖ ØŖØĢŲ†Ø§ØĄ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØĩŲˆØąØŠ", + "error_saving_image": "ØŽØˇØŖ: {error}", + "error_tag_face_bounding_box": "ØŽØˇØŖ ؁؊ ؈ØļØš ØšŲ„Ø§Ų…ØŠ ØšŲ„Ų‰ Ø§Ų„ŲˆØŦŲ‡ - Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ ØĨحداØĢŲŠØ§ØĒ Ø§Ų„Ų…ØąØ¨Øš Ø§Ų„Ų…Ø­ŲŠØˇ", "error_title": "ØŽØˇØŖ - حدØĢ ØŽŲ„Ų„ŲŒ Ų…Ø§", "errors": { "cannot_navigate_next_asset": "Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų„Ø§Ų†ØĒŲ‚Ø§Ų„ ØĨŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ Ø§Ų„ØĒØ§Ų„ŲŠ", @@ -746,10 +873,12 @@ "failed_to_keep_this_delete_others": "ŲØ´Ų„ ؁؊ Ø§Ų„Ø§Ø­ØĒŲØ§Ø¸ Ø¨Ų‡Ø°Ø§ Ø§Ų„ØŖØĩŲ„ ŲˆØ­Ø°Ų Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŖØŽØąŲ‰", "failed_to_load_asset": "ŲØ´Ų„ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰", "failed_to_load_assets": "ŲØ´Ų„ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ", + "failed_to_load_notifications": "ŲØ´Ų„ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØĨØ´ØšØ§ØąØ§ØĒ", "failed_to_load_people": "ŲØ´Ų„ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØŖØ´ØŽØ§Øĩ", "failed_to_remove_product_key": "ØĒØšØ°Øą ØĨØ˛Ø§Ų„ØŠ ؅؁ØĒاح Ø§Ų„Ų…Ų†ØĒØŦ", "failed_to_stack_assets": "ŲØ´Ų„ ؁؊ ØĒŲƒØ¯ŲŠØŗ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ", "failed_to_unstack_assets": "ŲØ´Ų„ ؁؊ ؁ØĩŲ„ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ", + "failed_to_update_notification_status": "ŲØ´Ų„ ؁؊ ØĒØ­Ø¯ŲŠØĢ Ø­Ø§Ų„ØŠ Ø§Ų„ØĨØ´ØšØ§Øą", "import_path_already_exists": "Ų…ØŗØ§Øą Ø§Ų„Ø§ØŗØĒŲŠØąØ§Ø¯ Ų‡Ø°Ø§ Ų…ŲˆØŦŲˆØ¯ Ų…ØŗØ¨Ų‚Ų‹Ø§.", "incorrect_email_or_password": "Ø¨ØąŲŠØ¯ ØŖŲˆ ŲƒŲ„Ų…ØŠ Ų…ØąŲˆØą ØēŲŠØą ØĩØ­ŲŠØ­ØŠ", "paths_validation_failed": "ŲØ´Ų„ ؁؊ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† {paths, plural, one {# Ų…ØŗØ§Øą} other {# Ų…ØŗØ§ØąØ§ØĒ}}", @@ -766,6 +895,7 @@ "unable_to_archive_unarchive": "ØĒØšØ°Øą {archived, select, true {Ø§Ų„ØŖØąØ´ŲØŠ} other {Ø§Ų„ØĨØŽØąØ§ØŦ Ų…Ų† Ø§Ų„ØŖØąØ´ŲŠŲ}}", "unable_to_change_album_user_role": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒØēŲŠŲŠØą Ø¯ŲˆØą Ų…ØŗØĒØŽØ¯Ų… Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "unable_to_change_date": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒØēŲŠŲŠØą Ø§Ų„ØĒØ§ØąŲŠØŽ", + "unable_to_change_description": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒØēŲŠŲŠØą Ø§Ų„ŲˆØĩ؁", "unable_to_change_favorite": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒØēŲŠŲŠØą Ø§Ų„Ų…ŲØļŲ„ØŠ Ų„Ų…Ø­ØĒŲˆŲ‰", "unable_to_change_location": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒØēŲŠŲŠØą Ø§Ų„Ų…ŲˆŲ‚Øš", "unable_to_change_password": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒØēŲŠŲŠØą ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", @@ -809,6 +939,7 @@ "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": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ Ø§ØŗØĒؚاد؊ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", @@ -842,6 +973,9 @@ "exif_bottom_sheet_location": "Ų…ŲˆŲ‚Øš", "exif_bottom_sheet_people": "Ø§Ų„Ų†Ø§Øŗ", "exif_bottom_sheet_person_add_person": "اØļ؁ Ø§ØŗŲ…Ø§", + "exif_bottom_sheet_person_age_months": "Ø§Ų„ØšŲ…Øą {months} Ø§Ø´Ų‡Øą", + "exif_bottom_sheet_person_age_year_months": "Ø§Ų„ØšŲ…Øą ŲĄ ØŗŲ†ØŠØŒ{months} Ø§Ø´Ų‡Øą", + "exif_bottom_sheet_person_age_years": "Ø§Ų„ØšŲ…Øą {years}", "exit_slideshow": "ØŽØąŲˆØŦ Ų…Ų† Ø§Ų„ØšØąØļ Ø§Ų„ØĒŲ‚Ø¯ŲŠŲ…ŲŠ", "expand_all": "ØĒŲˆØŗŲŠØš Ø§Ų„ŲƒŲ„", "experimental_settings_new_asset_list_subtitle": "ØŖØšŲ…Ø§Ų„ ØŦØ§ØąŲŠØŠ", @@ -858,10 +992,15 @@ "extension": "Ø§Ų„ØĨŲ…ØĒداد", "external": "ØŽØ§ØąØŦ؊", "external_libraries": "Ø§Ų„Ų…ŲƒØĒباØĒ Ø§Ų„ØŽØ§ØąØŦŲŠØŠ", - "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", + "external_network": "Ø´Ø¨ŲƒØŠ ØŽØ§ØąØŦŲŠØŠ", + "external_network_sheet_info": "ØšŲ†Ø¯Ų…Ø§ Ų„Ø§ ؊ØĒŲˆØ§ØŦد ØšŲ„Ų‰ Ø´Ø¨ŲƒØŠ Wi-Fi Ø§Ų„Ų…ŲØļŲ„ØŠØŒ ؁ØĨŲ†Ų‡ ØŗŲŠØĒØĩŲ„ Ø¨Ø§Ų„ØŽØ§Ø¯Ų… Ų…Ų† ØŽŲ„Ø§Ų„ ØŖŲˆŲ„ ØšŲ†Ø§ŲˆŲŠŲ† URL ØŖØ¯Ų†Ø§Ų‡ Ø§Ų„ØĒ؊ ŲŠŲ…ŲƒŲ†Ų‡ Ø§Ų„ŲˆØĩŲˆŲ„ ØĨŲ„ŲŠŲ‡Ø§ØŒ Ø¨Ø¯ØĄŲ‹Ø§ Ų…Ų† Ø§Ų„ØŖØšŲ„Ų‰ ØĨŲ„Ų‰ Ø§Ų„ØŖØŗŲŲ„", "face_unassigned": "ØēŲŠØą Ų…ØšŲŠŲ†", + "failed": "ŲØ´Ų„", + "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": "Ų„Ų… ؊ØĒŲ… Ø§Ų„ØšØĢŲˆØą ØšŲ„Ų‰ Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„Ų…ŲØļŲ„ØŠ", @@ -872,18 +1011,26 @@ "file_name_or_extension": "Ø§ØŗŲ… Ø§Ų„Ų…Ų„Ų ØŖŲˆ Ø§Ų…ØĒØ¯Ø§Ø¯Ų‡", "filename": "Ø§ØŗŲ… Ø§Ų„Ų…Ų„Ų", "filetype": "Ų†ŲˆØš Ø§Ų„Ų…Ų„Ų", + "filter": "ØĒØĩŲŲŠØŠ", "filter_people": "ØĒØĩŲŲŠØŠ Ø§Ų„Ø§Ø´ØŽØ§Øĩ", + "filter_places": "ØĒØĩŲŲŠØŠ Ø§Ų„Ø§Ų…Ø§ŲƒŲ†", "find_them_fast": "ŲŠŲ…ŲƒŲ†Ųƒ Ø§Ų„ØšØĢŲˆØą ØšŲ„ŲŠŲ‡Ø§ Ø¨ØŗØąØšØŠ Ø¨Ø§Ų„Ø§ØŗŲ… Ų…Ų† ØŽŲ„Ø§Ų„ Ø§Ų„Ø¨Ø­ØĢ", "fix_incorrect_match": "ØĨØĩŲ„Ø§Ø­ Ø§Ų„Ų…ØˇØ§Ø¨Ų‚ØŠ ØēŲŠØą Ø§Ų„ØĩØ­ŲŠØ­ØŠ", + "folder": "Ų…ØŦŲ„Ø¯", + "folder_not_found": "Ų„Ų… ؊ØĒŲ… Ø§Ų„ØšØĢŲˆØą ØšŲ„Ų‰ Ø§Ų„Ų…ØŦŲ„Ø¯", "folders": "Ø§Ų„Ų…ØŦŲ„Ø¯Ø§ØĒ", "folders_feature_description": "ØĒØĩŲØ­ ØšØąØļ Ø§Ų„Ų…ØŦŲ„Ø¯ Ų„Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…ŲˆØŦŲˆØ¯ØŠ ØšŲ„Ų‰ Ų†Ø¸Ø§Ų… Ø§Ų„Ų…Ų„ŲØ§ØĒ", "forward": "ØĨŲ„Ų‰ Ø§Ų„ØŖŲ…Ø§Ų…", + "gcast_enabled": "ŲƒŲˆŲƒŲ„ ŲƒØ§ØŗØĒ", + "gcast_enabled_description": "ØĒŲ‚ŲˆŲ… Ų‡Ø°Ų‡ Ø§Ų„Ų…ŲŠØ˛ØŠ بØĒØ­Ų…ŲŠŲ„ Ø§Ų„Ų…ŲˆØ§ØąØ¯ Ø§Ų„ØŽØ§ØąØŦŲŠØŠ Ų…Ų† Google Ø­ØĒŲ‰ ØĒØšŲ…Ų„.", "general": "ØšØ§Ų…", "get_help": "Ø§Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ Ø§Ų„Ų…ØŗØ§ØšØ¯ØŠ", + "get_wifiname_error": "ØĒØšØ°Øą Ø§Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ Ø§ØŗŲ… Ø´Ø¨ŲƒØŠ Wi-Fi. ØĒØŖŲƒØ¯ Ų…Ų† Ų…Ų†Ø­ Ø§Ų„ØŖØ°ŲˆŲ†Ø§ØĒ Ø§Ų„Ų„Ø§Ø˛Ų…ØŠ ŲˆØ§ØĒØĩØ§Ų„Ųƒ Ø¨Ø´Ø¨ŲƒØŠ Wi-Fi", "getting_started": "Ø§Ų„Ø¨Ø¯ØĄ", "go_back": "Ø§Ų„ØąØŦŲˆØš Ų„Ų„ØŽŲ„Ų", "go_to_folder": "Ø§Ø°Ų‡Ø¨ ØĨŲ„Ų‰ Ø§Ų„Ų…ØŦŲ„Ø¯", "go_to_search": "Ø§Ø°Ų‡Ø¨ ØĨŲ„Ų‰ Ø§Ų„Ø¨Ø­ØĢ", + "grant_permission": "Ų…Ų†Ø­ Ø§Ų„Ø§Ø°Ų†", "group_albums_by": "ØĒØŦŲ…ŲŠØš Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ø­ØŗØ¨...", "group_country": "Ų…ØŦŲ…ŲˆØšØŠ Ø§Ų„Ø¨Ų„Ø¯", "group_no": "Ø¨Ø¯ŲˆŲ† ØĒØŦŲ…ŲŠØš", @@ -893,6 +1040,12 @@ "haptic_feedback_switch": "ØĒŲ…ŲƒŲŠŲ† ØąØ¯ŲˆØ¯ Ø§Ų„ŲØšŲ„ Ø§Ų„Ų„Ų…ØŗŲŠØŠ", "haptic_feedback_title": "ØąØ¯ŲˆØ¯ ŲØšŲ„ Ų„Ų…ØŗŲŠØŠ", "has_quota": "Ų…Ø­Ø¯Ø¯ بحØĩØŠ", + "header_settings_add_header_tip": "اØļØ§Ų ØąØ§Øŗ", + "header_settings_field_validator_msg": "Ø§Ų„Ų‚ŲŠŲ…ØŠ Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų† ØĒŲƒŲˆŲ† ŲØ§ØąØēØŠ", + "header_settings_header_name_input": "Ø§ØŗŲ… Ø§Ų„ØąØŖØŗ", + "header_settings_header_value_input": "Ų‚ŲŠŲ…ØŠ Ø§Ų„ØąØŖØŗ", + "headers_settings_tile_subtitle": "Ų‚Ų… بØĒØšØąŲŠŲ ØąØ¤ŲˆØŗ Ø§Ų„ŲˆŲƒŲŠŲ„ Ø§Ų„ØĒ؊ ؊ØŦب ØŖŲ† ŲŠØąØŗŲ„Ų‡Ø§ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ Ų…Øš ŲƒŲ„ ØˇŲ„Ø¨ Ø´Ø¨ŲƒØŠ", + "headers_settings_tile_title": "ØąØ¤ŲˆØŗ ŲˆŲƒŲŠŲ„ Ų…ØŽØĩØĩØŠ", "hi_user": "Ų…ØąØ­Ø¨Ø§ {name} ({email})", "hide_all_people": "ØĨØŽŲØ§ØĄ ØŦŲ…ŲŠØš Ø§Ų„ØŖØ´ØŽØ§Øĩ", "hide_gallery": "Ø§ØŽŲØ§ØĄ Ø§Ų„Ų…ØšØąØļ", @@ -911,11 +1064,16 @@ "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": "Ų„Ø§ ŲŠŲ…ŲƒŲ† Ų…Ø´Ø§ØąŲƒØŠ Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„Ų…Ø­Ų„ŲŠØŠ ØšØ¨Øą Ø§Ų„ØąØ§Ø¨Øˇ ، ØŗŲˆŲ ؊ØĒØŽØˇŲ‰", "home_page_upload_err_limit": "Ų„Ø§ ŲŠŲ…ŲƒŲ† ØĨŲ„Ø§ ØĒØ­Ų…ŲŠŲ„ 30 ØŖØ­Ø¯ Ø§Ų„ØŖØĩŲˆŲ„ ؁؊ ŲˆŲ‚ØĒ ŲˆØ§Ø­Ø¯ ، ØŗŲˆŲ ؊ØĒØŽØˇŲ‰", "host": "Ø§Ų„Ų…Øļ؊؁", "hour": "ØŗØ§ØšØŠ", + "id": "Ø§Ų„Ų…ØšØąŲ", + "ignore_icloud_photos": "ØĒØŦØ§Ų‡Ų„ ØĩŲˆØą iCloud", + "ignore_icloud_photos_description": "Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØŽØ˛Ų†ØŠ ؁؊ Cloud Ų„Ų† ؊ØĒŲ… ØĒØ­Ų…ŲŠŲ„Ų‡Ø§ ØĨŲ„Ų‰ ØŽØ§Ø¯Ų… Immich", "image": "ØĩŲˆØąØŠ", "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} ØĒŲ… Ø§Ų„ØĒŲ‚Ø§ØˇŲ‡Ø§ ؁؊ {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} ØĒŲ… Ø§Ų„ØĒŲ‚Ø§ØˇŲ‡Ø§ Ų…Øš {person1} ؁؊ {date}", @@ -927,6 +1085,7 @@ "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} ØĒŲ… Ø§Ų„ØĒŲ‚Ø§ØˇŲ‡Ø§ ؁؊ {city}، {country} Ų…Øš {person1} ؈{person2} ؁؊ {date}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} ØĒŲ… Ø§Ų„ØĒŲ‚Ø§ØˇŲ‡Ø§ ؁؊ {city}، {country} Ų…Øš {person1}، {person2}، ؈{person3} ؁؊ {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} ØĒŲ… Ø§Ų„ØĒŲ‚Ø§ØˇŲ‡Ø§ ؁؊ {city}, {country} with {person1}, {person2}, Ų…Øš {additionalCount, number} ØĸØŽØąŲŠŲ† ؁؊ {date}", + "image_saved_successfully": "Ø§Ų„ØĩŲˆØą Ø­ŲŲØ¸ØĒ", "image_viewer_page_state_provider_download_started": "Ø¨Ø¯ØŖ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", "image_viewer_page_state_provider_download_success": "ØĒŲ… Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„ Ø¨Ų†ØŦاح", "image_viewer_page_state_provider_share_error": "ØŽØˇØŖ ؁؊ Ø§Ų„Ų…Ø´Ø§ØąŲƒØŠ", @@ -948,8 +1107,16 @@ "night_at_midnight": "ŲƒŲ„ Ų„ŲŠŲ„ØŠ ØšŲ†Ø¯ Ų…Ų†ØĒØĩ؁ Ø§Ų„Ų„ŲŠŲ„", "night_at_twoam": "ŲƒŲ„ Ų„ŲŠŲ„ØŠ Ø§Ų„ØŗØ§ØšØŠ 2 Øĩباحا" }, + "invalid_date": "ØĒØ§ØąŲŠØŽ ØēŲŠØą ØĩØ§Ų„Ø­", + "invalid_date_format": "Øĩ؊ØēØŠ ØĒØ§ØąŲŠØŽ ØēŲŠØą ØĩØ§Ų„Ø­ØŠ", "invite_people": "Ø¯ØšŲˆØŠ Ø§Ų„ØŖØ´ØŽØ§Øĩ", "invite_to_album": "Ø¯ØšŲˆØŠ ØĨŲ„Ų‰ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", + "ios_debug_info_fetch_ran_at": "ØŦØąØĒ ØšŲ…Ų„ŲŠØŠ Ø§Ų„ØŦŲ„Ø¨ ؁؊ {dateTime}", + "ios_debug_info_last_sync_at": "Ø§ØŽØą Ų…Ø˛Ø§Ų…Ų†ØŠ {dateTime}", + "ios_debug_info_no_processes_queued": "Ų„Ø§ ØĒ؈ØŦد ØšŲ…Ų„ŲŠØ§ØĒ ØŽŲ„ŲŲŠØŠ ؁؊ Ų‚Ø§ØĻŲ…ØŠ Ø§Ų„Ø§Ų†ØĒØ¸Ø§Øą", + "ios_debug_info_no_sync_yet": "Ų„Ų… ؊ØĒŲ… ØĒØ´ØēŲŠŲ„ ØŖŲŠ Ų…Ų‡Ų…ØŠ Ų…Ø˛Ø§Ų…Ų†ØŠ ؁؊ Ø§Ų„ØŽŲ„ŲŲŠØŠ Ø­ØĒŲ‰ Ø§Ų„ØĸŲ†", + "ios_debug_info_processes_queued": "{count, plural, one {{count} ØšŲ…Ų„ŲŠØŠ ØŽŲ„ŲŲŠØŠ Ø§Ø¯ØŽŲ„ØĒØŠŲŲŠ ØˇØ§Ø¨ŲˆØą} other {{count} ØšŲ…Ų„ŲŠØ§ØĒ ØŽŲ„ŲŲŠØŠ Ø§Ø¯ØŽŲ„ØĒ ؁؊ ØˇØ§Ø¨ŲˆØą}}", + "ios_debug_info_processing_ran_at": "Ø§Ų„Ų…ØšØ§Ų„ØŦØŠ ØŦØąØĒ ؁؊ {dateTime}", "items_count": "{count, plural, one {# ØšŲ†ØĩØą} other {# ØšŲ†Ø§ØĩØą}}", "jobs": "Ø§Ų„ŲˆØ¸Ø§ØĻ؁", "keep": "احØĒŲØ¸", @@ -958,6 +1125,9 @@ "kept_this_deleted_others": "ØĒŲ… Ø§Ų„Ø§Ø­ØĒŲØ§Ø¸ Ø¨Ų‡Ø°Ø§ Ø§Ų„ØŖØĩŲ„ ŲˆØ­Ø°Ų {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "ا؎ØĒØĩØ§ØąØ§ØĒ Ų„ŲˆØ­ØŠ Ø§Ų„Ų…ŲØ§ØĒŲŠØ­", "language": "Ø§Ų„Ų„ØēØŠ", + "language_no_results_subtitle": "Ø­Ø§ŲˆŲ„ ØĒØšØ¯ŲŠŲ„ Ų…ØĩØˇŲ„Ø­ Ø§Ų„Ø¨Ø­ØĢ", + "language_no_results_title": "Ų„Ų… ؊ØĒŲ… Ø§Ų„ØšØĢŲˆØą ØšŲ„Ų‰ Ų„ØēاØĒ", + "language_search_hint": "Ø§Ų„Ø¨Ø­ØĢ ØšŲ† Ų„ØēاØĒ...", "language_setting_description": "ا؎ØĒØą Ų„ØēØĒ؃ Ø§Ų„Ų…ŲØļŲ„ØŠ", "last_seen": "Ø§ØŽØą Ø¸Ų‡ŲˆØą", "latest_version": "احدØĢ اØĩØ¯Ø§Øą", @@ -983,21 +1153,29 @@ "list": "Ų‚Ø§ØĻŲ…ØŠ", "loading": "ØĒØ­Ų…ŲŠŲ„", "loading_search_results_failed": "ŲØ´Ų„ ØĒØ­Ų…ŲŠŲ„ Ų†ØĒاØĻØŦ Ø§Ų„Ø¨Ø­ØĢ", - "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", + "local_asset_cast_failed": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ بØĢ ØŖØĩŲ„ Ų„Ų… ؊ØĒŲ… ØĒØ­Ų…ŲŠŲ„Ų‡ ØĨŲ„Ų‰ Ø§Ų„ØŽØ§Ø¯Ų…", + "local_network": "Ø´Ø¨ŲƒØŠ Ų…Ø­Ų„ŲŠØŠ", + "local_network_sheet_info": "ØŗŲŠØĒØĩŲ„ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ Ø¨Ø§Ų„ØŽØ§Ø¯Ų… Ų…Ų† ØŽŲ„Ø§Ų„ ØšŲ†ŲˆØ§Ų† URL Ų‡Ø°Ø§ ØšŲ†Ø¯ Ø§ØŗØĒØŽØ¯Ø§Ų… Ø´Ø¨ŲƒØŠ Wi-Fi Ø§Ų„Ų…Ø­Ø¯Ø¯ØŠ", + "location_permission": "Ø§Ø°Ų† Ø§Ų„Ų…ŲˆŲ‚Øš", + "location_permission_content": "Ų…Ų† ØŖØŦŲ„ Ø§ØŗØĒØŽØ¯Ø§Ų… Ų…ŲŠØ˛ØŠ Ø§Ų„ØĒØ¨Ø¯ŲŠŲ„ Ø§Ų„ØĒŲ„Ų‚Ø§ØĻŲŠØŒ ŲŠØ­ØĒاØŦ Immich ØĨŲ„Ų‰ ØĨØ°Ų† Ų…ŲˆŲ‚Øš Ø¯Ų‚ŲŠŲ‚ Ø­ØĒŲ‰ ؊ØĒŲ…ŲƒŲ† Ų…Ų† Ų‚ØąØ§ØĄØŠ Ø§ØŗŲ… Ø´Ø¨ŲƒØŠ Wi-Fi Ø§Ų„Ø­Ø§Ų„ŲŠØŠ", "location_picker_choose_on_map": "ا؎ØĒØą ØšŲ„Ų‰ Ø§Ų„ØŽØąŲŠØˇØŠ", "location_picker_latitude_error": "ØŖØ¯ØŽŲ„ ØŽØˇ ØšØąØļ ØĩØ§Ų„Ø­", "location_picker_latitude_hint": "ØŖØ¯ØŽŲ„ ØŽØˇ Ø§Ų„ØšØąØļ Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ Ų‡Ų†Ø§", "location_picker_longitude_error": "ØŖØ¯ØŽŲ„ ØŽØˇ Ø§Ų„ØˇŲˆŲ„ Ø§Ų„ØĩØ­ŲŠØ­", "location_picker_longitude_hint": "ØŖØ¯ØŽŲ„ ØŽØˇ Ø§Ų„ØˇŲˆŲ„ Ų‡Ų†Ø§", + "lock": "؂؁؄", + "locked_folder": "Ų…ØŦŲ„Ø¯ Ų…Ų‚ŲŲˆŲ„", "log_out": "ØĒØŗØŦŲŠŲ„ ØŽØąŲˆØŦ", "log_out_all_devices": "ØĒØŗØŦŲŠŲ„ Ø§Ų„ØŽØąŲˆØŦ Ų…Ų† ŲƒØ§ŲØŠ Ø§Ų„ØŖØŦŲ‡Ø˛ØŠ", + "logged_in_as": "ØĒŲ… ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ Ø¨Ø§ØŗŲ… {user}", "logged_out_all_devices": "ØĒŲ… ØĒØŗØŦŲŠŲ„ Ø§Ų„ØŽØąŲˆØŦ Ų…Ų† ØŦŲ…ŲŠØš Ø§Ų„ØŖØŦŲ‡Ø˛ØŠ", "logged_out_device": "ØĒŲ… ØĒØŗØŦŲŠŲ„ Ø§Ų„ØŽØąŲˆØŦ Ų…Ų† Ø§Ų„ØŦŲ‡Ø§Ø˛", "login": "ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„", "login_disabled": "ØĒŲ… ØĒØšØˇŲŠŲ„ ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„", - "login_form_api_exception": " Ø§ØŗØĒØĢŲ†Ø§ØĄ Ø¨ØąŲ…ØŦØŠ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚Ø§ØĒ. ŲŠØąØŦŲ‰ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† ØšŲ†ŲˆØ§Ų† Ø§Ų„ØŽØ§Ø¯Ų… ŲˆØ§Ų„Ų…Ø­Ø§ŲˆŲ„ØŠ Ų…ØąØŠ ØŖØŽØąŲ‰ ", + "login_form_api_exception": "Ø§ØŗØĒØĢŲ†Ø§ØĄ API. ŲŠØąØŦŲ‰ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† ØšŲ†ŲˆØ§Ų† URL Ø§Ų„ØŽØ§Ø¯Ų… ŲˆØ§Ų„Ų…Ø­Ø§ŲˆŲ„ØŠ Ų…ØąØŠ ØŖØŽØąŲ‰.", "login_form_back_button_text": "Ø§Ų„ØąØŦŲˆØš Ų„Ų„ØŽŲ„Ų", "login_form_email_hint": "yoursemail@email.com", + "login_form_endpoint_hint": "http://Ø§Ų„Ų…Ų†ŲØ°:ØšŲ†ŲˆØ§Ų†â€Ģ-ip-Ø§Ų„ØŽØ§Ø¯Ų…", "login_form_endpoint_url": "url Ų†Ų‚ØˇØŠ Ų†Ų‡Ø§ŲŠØŠ Ø§Ų„ØŽØ§Ø¯Ų…", "login_form_err_http": "ŲŠØąØŦŲ‰ ØĒØ­Ø¯ŲŠØ¯ http:// ØŖŲˆ https://", "login_form_err_invalid_email": "Ø¨ØąŲŠØ¯ ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ ØŽØ§ØˇØĻ", @@ -1021,7 +1199,8 @@ "look": "Ø§Ų„Ø´ŲƒŲ„", "loop_videos": "ØĒŲƒØąØ§Øą Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ", "loop_videos_description": "ŲŲŽØšŲ’Ų„ Ų„ØĒŲƒØąØ§Øą Ų…Ų‚ØˇØš ŲŲŠØ¯ŲŠŲˆ ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§ ؁؊ ØšØ§ØąØļ Ø§Ų„ØĒŲØ§ØĩŲŠŲ„.", - "main_branch_warning": "ØŖŲ†ØĒ ØĒØŗØĒØŽØ¯Ų… ØĨØĩØ¯Ø§ØąØ§Ų‹ ØĒØˇŲˆŲŠØąŲŠØ§Ų‹Ø› ŲˆŲ†Ø­Ų† Ų†ŲˆØĩ؊ بشد؊ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… ØĨØĩØ¯Ø§Øą Ø§Ų„Ų†Ø´Øą!", + "main_branch_warning": "ØŖŲ†ØĒ ØĒØŗØĒØŽØ¯Ų… ØĨØĩØ¯Ø§ØąØ§Ų‹ Ų‚ŲŠØ¯ Ø§Ų„ØĒØˇŲˆŲŠØąØ› ŲˆŲ†Ø­Ų† Ų†ŲˆØĩ؊ بشد؊ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… ØĨØĩØ¯Ø§Øą Ø§Ų„Ų†Ø´Øą!", + "main_menu": "Ø§Ų„Ų‚Ø§ØĻŲ…ØŠ Ø§Ų„ØąØĻŲŠØŗŲŠØŠ", "make": "ØĩŲ†Øš", "manage_shared_links": "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØąŲˆØ§Ø¨Øˇ Ø§Ų„Ų…Ø´ØĒØąŲƒØŠ", "manage_sharing_with_partners": "ØĨØ¯Ø§ØąØŠ Ø§Ų„Ų…Ø´Ø§ØąŲƒØŠ Ų…Øš Ø§Ų„Ø´ØąŲƒØ§ØĄ", @@ -1031,6 +1210,8 @@ "manage_your_devices": "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØŖØŦŲ‡Ø˛ØŠ Ø§Ų„ØĒ؊ ØĒŲ… ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ ØĨŲ„ŲŠŲ‡Ø§", "manage_your_oauth_connection": "ØĨØ¯Ø§ØąØŠ اØĒØĩØ§Ų„ OAuth Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ", "map": "Ø§Ų„ØŽØąŲŠØˇØŠ", + "map_assets_in_bound": "{count} ØĩŲˆØąŲ‡", + "map_assets_in_bounds": "{count} ØĩŲˆØą", "map_cannot_get_user_location": "Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ Ų…ŲˆŲ‚Øš Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", "map_location_dialog_yes": "Ų†ØšŲ…", "map_location_picker_page_use_location": "Ø§ØŗØĒØŽØ¯Ų… Ų‡Ø°Ø§ Ø§Ų„Ų…ŲˆŲ‚Øš", @@ -1044,13 +1225,18 @@ "map_settings": "ØĨؚداداØĒ Ø§Ų„ØŽØąŲŠØˇØŠ", "map_settings_dark_mode": "Ø§Ų„ŲˆØļØš Ø§Ų„Ų…Ø¸Ų„Ų…", "map_settings_date_range_option_day": "24 ØŗØ§ØšØŠ Ø§Ų„Ų…Ø§ØļŲŠØŠ", + "map_settings_date_range_option_days": "Ø§Ų„Ø§ŲŠØ§Ų… {days} Ø§Ų„Ų…Ø§ØļŲŠØŠ", "map_settings_date_range_option_year": "Ø§Ų„ØŗŲ†ØŠ Ø§Ų„ŲØ§ØĻØĒØŠ", + "map_settings_date_range_option_years": "Ø§Ų„ØŗŲ†ŲˆØ§ØĒ {years} Ø§Ų„Ų…Ø§ØļŲŠØŠ", "map_settings_dialog_title": "ØĨؚداداØĒ Ø§Ų„ØŽØąŲŠØˇØŠ", "map_settings_include_show_archived": "ØĒØ´Ų…Ų„ Ø§Ų„ØŖØąØ´ŲØŠ", "map_settings_include_show_partners": "ØĒØļŲ…ŲŠŲ† Ø§Ų„Ø´ØąŲƒØ§ØĄ", "map_settings_only_show_favorites": "Ø§Ø¸Ų‡Ø§Øą Ø§Ų„Ų…ŲØļŲ„ØŠ ŲŲ‚Øˇ", "map_settings_theme_settings": "Ų…Ø¸Ų‡Øą Ø§Ų„ØŽØąŲŠØˇØŠ", "map_zoom_to_see_photos": "Ų‚Ų… بØĒØĩØēŲŠØąŲ‡Ø§ Ų„ØąØ¤ŲŠØŠ Ø§Ų„ØĩŲˆØą", + "mark_all_as_read": "ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ŲƒŲ„ ŲƒŲ…Ų‚ØąŲˆØĄ", + "mark_as_read": "ØĒØ­Ø¯ŲŠØ¯ ŲƒŲ…Ų‚ØąŲˆØĄ", + "marked_all_as_read": "ØĒŲ… ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ŲƒŲ„ ŲƒŲ…Ų‚ØąŲˆØĄ", "matches": "ØĒØˇØ§Ø¨Ų‚Ø§ØĒ", "media_type": "Ų†ŲˆØš Ø§Ų„ŲˆØŗØ§ØĻØˇ", "memories": "Ø§Ų„Ø°ŲƒØąŲŠØ§ØĒ", @@ -1075,6 +1261,13 @@ "month": "Ø´Ų‡Øą", "monthly_title_text_date_format": "Øˇ Øˇ Øˇ", "more": "Ø§Ų„Ų…Ø˛ŲŠØ¯", + "move": "ØĒØ­ØąŲŠŲƒ", + "move_off_locked_folder": "ØĒØ­ØąŲŠŲƒ ØŽØ§ØąØŦ Ø§Ų„Ų…ØŦŲ„Ø¯ Ø§Ų„Ų…Ų‚ŲŲ„", + "move_to_lock_folder_action_prompt": "{count} اØļ؊؁ ØĨŲ„Ų‰ Ø§Ų„Ų…ØŦŲ„Ø¯ Ø§Ų„Ų…Ų‚ŲŲ„", + "move_to_locked_folder": "Ø§Ų„Ų†Ų‚Ų„ Ø§Ų„Ų‰ Ų…ØŦŲ„Ø¯ Ų…ØēŲ„Ų‚", + "move_to_locked_folder_confirmation": "Ų‡Ø°Ų‡ Ø§Ų„ØĩŲˆØą ŲˆØ§Ų„ŲØ¯ŲŠŲˆØ§ØĒ ØŗØĒØĒŲ… Ø§Ø˛Ø§Ų„ØĒŲ‡Ø§ Ų…Ų† ØŦŲ…ŲŠØš Ø§Ų„Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ، ŲˆŲŠŲ…ŲƒŲ†Ø§Ų† ØĒØĒŲ… Ų…Ø´Ø§Ų‡Ø¯ØĒŲ‡Ø§ ŲŲ‚Øˇ Ų…Ų† ØŽŲ„Ø§Ų„ Ø§Ų„Ų…ØŦŲ„Ø¯ Ø§Ų„Ų…Ų‚ŲŲ„", + "moved_to_archive": "ØĒŲ… Ų†Ų‚Ų„ {count, plural, one {# اØĩŲ„} other {# اØĩŲˆŲ„}} Ø§Ų„Ų‰ Ø§Ų„Ø§ØąØ´ŲŠŲ", + "moved_to_library": "ØĒŲ… Ų†Ų‚Ų„ {count, plural, one {# اØĩŲ„} other {# اØĩŲˆŲ„}} Ø§Ų„Ų‰ Ø§Ų„Ų…ŲƒØĒب؊", "moved_to_trash": "ØĒŲ… Ø§Ų„Ų†Ų‚Ų„ ØĨŲ„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "multiselect_grid_edit_date_time_err_read_only": "Ų„Ø§ ŲŠŲ…ŲƒŲ† ØĒØšØ¯ŲŠŲ„ ØĒØ§ØąŲŠØŽ Ø§Ų„ØŖØĩŲˆŲ„ (Ø§Ų„Ų…ŲˆØ§Ø¯) Ų„Ų„Ų‚ØąØ§ØĄØŠ ŲŲ‚ØˇØŒ ØŗŲˆŲ ؊ØĒØŽØˇŲ‰", "multiselect_grid_edit_gps_err_read_only": "Ų„Ø§ ŲŠŲ…ŲƒŲ† ØĒØšØ¯ŲŠŲ„ Ų…ŲˆŲ‚Øš Ø§Ų„ØŖØĩŲˆŲ„ (Ø§Ų„Ų…ŲˆØ§Ø¯) Ų„Ų„Ų‚ØąØ§ØĄØŠ ŲŲ‚ØˇØŒ ØŗŲˆŲ ؊ØĒØŽØˇŲ‰", @@ -1082,12 +1275,15 @@ "my_albums": "ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ؊", "name": "Ø§Ų„Ø§ØŗŲ…", "name_or_nickname": "Ø§Ų„Ø§ØŗŲ… ØŖŲˆ Ø§Ų„Ų„Ų‚Ø¨", + "networking_settings": "Ø§Ų„Ø´Ø¨ŲƒØ§ØĒ", + "networking_subtitle": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ Ų†Ų‚ØˇØŠ Ø§Ų„ØŽØ§Ø¯Ų… Ø§Ų„Ų†Ų‡Ø§ØĻŲŠØŠ", "never": "ØŖØ¨Ø¯Ø§Ų‹", "new_album": "Ø§Ų„Ø¨ŲˆŲ… ØŦØ¯ŲŠØ¯", "new_api_key": "؅؁ØĒاح API ØŦØ¯ŲŠØ¯", "new_password": "ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ", "new_person": "Ø´ØŽØĩ ØŦØ¯ŲŠØ¯", - "new_pin_code": "Ø§Ų„ØąŲ‚Ų… Ø§Ų„ØŗØąŲŠ Ø§Ų„ØŦØ¯ŲŠØ¯", + "new_pin_code": "ØąŲ…Ø˛ PIN Ø§Ų„ØŦØ¯ŲŠØ¯", + "new_pin_code_subtitle": "Ų‡Ø°Ų‡ ØŖŲˆŲ„ Ų…ØąØŠ ØĒØ¯ØŽŲ„ ŲŲŠŲ‡Ø§ ØĨŲ„Ų‰ Ø§Ų„Ų…ØŦŲ„Ø¯ Ø§Ų„Ų…Ų‚ŲŲ„. ØŖŲ†Ø´ØĻ ØąŲ…Ø˛Ų‹Ø§ PIN Ų„Ų„ŲˆØĩŲˆŲ„ Ø¨Ø§Ų…Ø§Ų† ØĨŲ„Ų‰ Ų‡Ø°Ų‡ Ø§Ų„ØĩŲØ­ØŠ", "new_user_created": "ØĒŲ… ØĨŲ†Ø´Ø§ØĄ Ų…ØŗØĒØŽØ¯Ų… ØŦØ¯ŲŠØ¯", "new_version_available": "ØĨØĩØ¯Ø§Øą ØŦØ¯ŲŠØ¯ Ų…ØĒاح", "newest_first": "Ø§Ų„ØŖØ­Ø¯ØĢ ØŖŲˆŲ„Ø§Ų‹", @@ -1100,19 +1296,25 @@ "no_archived_assets_message": "ØŖØąØ´ŲØŠ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ų„ØĨØŽŲØ§ØĻŲ‡Ø§ Ų…Ų† ØšØąØļ Ø§Ų„ØĩŲˆØą Ų„Ø¯ŲŠŲƒ", "no_assets_message": "Ø§Ų†Ų‚Øą Ų„ØĒØ­Ų…ŲŠŲ„ ØĩŲˆØąØĒ؃ Ø§Ų„ØŖŲˆŲ„Ų‰", "no_assets_to_show": "Ų„Ø§ ØĒ؈ØŦد ØŖØĩŲˆŲ„ Ų„ØšØąØļŲ‡Ø§", + "no_cast_devices_found": "Ų„Ų… ؊ØĒŲ… Ø§ŲŠØŦاد ØŦŲ‡Ø§Ø˛ بØĢ", "no_duplicates_found": "Ų„Ų… ؊ØĒŲ… Ø§Ų„ØšØĢŲˆØą ØšŲ„Ų‰ ØŖŲŠ ØĒŲƒØąØ§ØąØ§ØĒ.", "no_exif_info_available": "Ų„Ø§ ØĒØĒŲˆŲØą Ų…ØšŲ„ŲˆŲ…Ø§ØĒ exif", "no_explore_results_message": "Ų‚Ų… Ø¨ØąŲØš Ø§Ų„Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„ØĩŲˆØą Ų„Ø§ØŗØĒŲƒØ´Ø§Ų Ų…ØŦŲ…ŲˆØšØĒ؃.", "no_favorites_message": "ØŖØļ؁ Ø§Ų„Ų…ŲØļŲ„ØŠ Ų„Ų„ØšØĢŲˆØą Ø¨ØŗØąØšØŠ ØšŲ„Ų‰ ØŖŲØļŲ„ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ", "no_libraries_message": "ØĨŲ†Ø´Ø§ØĄ Ų…ŲƒØĒب؊ ØŽØ§ØąØŦŲŠØŠ Ų„ØšØąØļ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ", + "no_locked_photos_message": "Ø§Ų„ØĩŲˆØą ŲˆØ§Ų„ŲØ¯ŲŠŲˆŲ‡Ø§ØĒ ؁؊ Ø§Ų„Ų…ØŦŲ„Ø¯ Ø§Ų„Ų…Ų‚ŲŲ„ Ų…ØŽŲŲŠØŠ ŲˆŲ„Ų† ØĒØĩŲ‡Øą ؁؊ Ø§Ų„ØĒØĩŲØ­ Ø§Ųˆ Ø§Ų„Ø¨Ø­ØĢ ؁؊ Ų…ŲƒØĒبØĒ؃.", "no_name": "Ų„Ø§ Ø§ØŗŲ…", + "no_notifications": "Ų„Ø§ ØĒ؈ØŦد ØĒŲ†Ø¨ŲŠŲ‡Ø§ØĒ", + "no_people_found": "Ų„Ų… ؊ØĒŲ… Ø§Ų„ØšØĢŲˆØą ØšŲ„Ų‰ اش؎اØĩ Ų…ØˇØ§Ø¨Ų‚ŲŠŲ†", "no_places": "Ų„Ø§ ØŖŲ…Ø§ŲƒŲ†", "no_results": "Ų„Ø§ ؊؈ØŦد Ų†ØĒاØĻØŦ", "no_results_description": "ØŦØąØ¨ ŲƒŲ„Ų…ØŠ ØąØĻŲŠØŗŲŠØŠ Ų…ØąØ§Ø¯ŲØŠ ØŖŲˆ ØŖŲƒØĢØą ØšŲ…ŲˆŲ…ŲŠØŠ", "no_shared_albums_message": "Ų‚Ų… بØĨŲ†Ø´Ø§ØĄ ØŖŲ„Ø¨ŲˆŲ… Ų„Ų…Ø´Ø§ØąŲƒØŠ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ų…Øš Ø§Ų„ØŖØ´ØŽØ§Øĩ ؁؊ Ø´Ø¨ŲƒØĒ؃", "not_in_any_album": "Ų„ŲŠØŗØĒ ؁؊ ØŖŲŠ ØŖŲ„Ø¨ŲˆŲ…", - "note_apply_storage_label_to_previously_uploaded assets": "Ų…Ų„Ø§Ø­Ø¸ØŠ: Ų„ØĒØˇØ¨ŲŠŲ‚ ØĒØŗŲ…ŲŠØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØĒŲ… ØąŲØšŲ‡Ø§ Ų…ØŗØ¨Ų‚Ų‹Ø§ØŒ Ų‚Ų… بØĒØ´ØēŲŠŲ„", + "not_selected": "Ų„Ų… ŲŠØŽØĒØ§Øą", + "note_apply_storage_label_to_previously_uploaded assets": "Ų…Ų„Ø§Ø­Ø¸ØŠ: Ų„ØĒØˇØ¨ŲŠŲ‚ ØŗŲ…ØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØĒŲ… ØąŲØšŲ‡Ø§ Ų…ØŗØ¨Ų‚Ų‹Ø§ØŒ Ų‚Ų… بØĒØ´ØēŲŠŲ„", "notes": "Ų…Ų„Ø§Ø­Ø¸Ø§ØĒ", + "nothing_here_yet": "Ų„Ø§ ؊؈ØŦد Ø´ŲŠØĄ Ų‡Ų†Ø§ بؚد", "notification_permission_dialog_content": "Ų„ØĒŲ…ŲƒŲŠŲ† Ø§Ų„ØĨØŽØˇØ§ØąØ§ØĒ ، Ø§Ų†ØĒŲ‚Ų„ ØĨŲ„Ų‰ Ø§Ų„ØĨؚداداØĒ ؈ ا؎ØĒØ§Øą Ø§Ų„ØŗŲ…Ø§Ø­.", "notification_permission_list_tile_content": "Ų…Ų†Ø­ ØĨØ°Ų† Ų„ØĒŲ…ŲƒŲŠŲ† Ø§Ų„ØĨØŽØˇØ§ØąØ§ØĒ.", "notification_permission_list_tile_enable_button": "ØĒŲ…ŲƒŲŠŲ† Ø§Ų„ØĨØŽØˇØ§ØąØ§ØĒ", @@ -1120,16 +1322,22 @@ "notification_toggle_setting_description": "ØĒŲØšŲŠŲ„ ØĨØ´ØšØ§ØąØ§ØĒ Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ", "notifications": "ØĨØ´ØšØ§ØąØ§ØĒ", "notifications_setting_description": "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØĨØ´ØšØ§ØąØ§ØĒ", + "oauth": "OAuth", "official_immich_resources": "Ø§Ų„Ų…ŲˆØ§ØąØ¯ Ø§Ų„ØąØŗŲ…ŲŠØŠ Ų„Ø´ØąŲƒØŠ Immich", "offline": "ØēŲŠØą Ų…ØĒØĩŲ„", "ok": "Ų†ØšŲ…", "oldest_first": "Ø§Ų„ØŖŲ‚Ø¯Ų… ØŖŲˆŲ„Ø§", + "on_this_device": "ØšŲ„Ų‰ Ų‡Ø°Ø§ Ø§Ų„ØŦŲ‡Ø§Ø˛", "onboarding": "Ø§Ų„ØĨؚداد Ø§Ų„ØŖŲˆŲ„ŲŠ", - "onboarding_privacy_description": "ØĒØšØĒŲ…Ø¯ Ø§Ų„Ų…ŲŠØ˛Ø§ØĒ Ø§Ų„ØĒØ§Ų„ŲŠØŠ (ا؎ØĒŲŠØ§ØąŲŠ) ØšŲ„Ų‰ ØŽØ¯Ų…Ø§ØĒ ØŽØ§ØąØŦŲŠØŠØŒ ŲˆŲŠŲ…ŲƒŲ† ØĒØšØˇŲŠŲ„Ų‡Ø§ ؁؊ ØŖŲŠ ŲˆŲ‚ØĒ ؁؊ ØĨؚداداØĒ Ø§Ų„ØĨØ¯Ø§ØąØŠ.", + "onboarding_locale_description": "ا؎ØĒØą Ų„ØēØĒ؃ Ø§Ų„Ų…ŲØļŲ„ØŠ. ŲŠŲ…ŲƒŲ†Ųƒ ØĒØēŲŠŲŠØąŲ‡Ø§ ŲŲŠŲ…Ø§ بؚد ؁؊ Ø§Ų„Ø§ØšØ¯Ø§Ø¯Ø§ØĒ.", + "onboarding_privacy_description": "ØĒØšØĒŲ…Ø¯ Ø§Ų„Ų…ŲŠØ˛Ø§ØĒ Ø§Ų„ØĒØ§Ų„ŲŠØŠ (ا؎ØĒŲŠØ§ØąŲŠ) ØšŲ„Ų‰ ØŽØ¯Ų…Ø§ØĒ ØŽØ§ØąØŦŲŠØŠØŒ ŲˆŲŠŲ…ŲƒŲ† ØĒØšØˇŲŠŲ„Ų‡Ø§ ؁؊ ØŖŲŠ ŲˆŲ‚ØĒ ؁؊ Ø§Ų„Ø§ØšØ¯Ø§Ø¯Ø§ØĒ.", + "onboarding_server_welcome_description": "Ų„Ų†Ų‚Ų… باؚداد Ų†ØŗØŽØĒ؃ Ų…Ų† Ø§Ų„Ø¨ØąŲ†Ø§Ų…ØŦ Ų…Øš بؚØļ Ø§Ų„Ø§ØšØ¯Ø§Ø¯Ø§ØĒ Ø§Ų„Ø´Ø§ØĻؚ؊.", "onboarding_theme_description": "ا؎ØĒØą Ų†ØŗŲ‚ Ø§Ų„ØŖŲ„ŲˆØ§Ų† Ų„Ų„Ų†ØŗØŽØŠ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ. ŲŠŲ…ŲƒŲ†Ųƒ ØĒØēŲŠŲŠØą Ø°Ų„Ųƒ Ų„Ø§Ø­Ų‚Ų‹Ø§ ؁؊ ØĨؚداداØĒ؃.", + "onboarding_user_welcome_description": "Ų„Ų†ØŗØ§ØšØ¯Ųƒ ØšŲ„Ų‰ Ø§Ų„Ø¨Ø¯ØĄ!", "onboarding_welcome_user": "Ų…ØąØ­Ø¨Ø§ØŒ {user}", "online": "Ų…ØĒØĩŲ„", "only_favorites": "Ø§Ų„Ų…ŲØļŲ„ØŠ ŲŲ‚Øˇ", + "open": "؁ØĒØ­", "open_in_map_view": "؁ØĒØ­ ؁؊ ØšØąØļ Ø§Ų„ØŽØąŲŠØˇØŠ", "open_in_openstreetmap": "؁ØĒØ­ ؁؊ OpenStreetMap", "open_the_search_filters": "Ø§ŲØĒØ­ Ų…ØąØ´Ø­Ø§ØĒ Ø§Ų„Ø¨Ø­ØĢ", @@ -1146,12 +1354,14 @@ "partner_can_access": "ŲŠØŗØĒØˇŲŠØš {partner} Ø§Ų„ŲˆØĩŲˆŲ„", "partner_can_access_assets": "ØŦŲ…ŲŠØš Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ Ø¨Ø§ØŗØĒØĢŲ†Ø§ØĄ ØĒŲ„Ųƒ Ø§Ų„Ų…ŲˆØŦŲˆØ¯ØŠ ؁؊ Ø§Ų„Ų…Ø¤ØąØ´ŲØŠ ŲˆØ§Ų„Ų…Ø­Ø°ŲˆŲØŠ", "partner_can_access_location": "Ø§Ų„Ų…ŲˆŲ‚Øš Ø§Ų„Ø°ŲŠ ØĒŲ… Ø§Ų„ØĒŲ‚Ø§Øˇ ØĩŲˆØąŲƒ ŲŲŠŲ‡", + "partner_list_user_photos": "ØĩŲˆØą {user}", "partner_list_view_all": "ØšØąØļ Ø§Ų„ŲƒŲ„", "partner_page_empty_message": "Ų„Ų… ؊ØĒŲ… Ų…Ø´Ø§ØąŲƒØŠ ØĩŲˆØąŲƒ بؚد Ų…Øš ØŖŲŠ Ø´ØąŲŠŲƒ.", "partner_page_no_more_users": "Ų„Ø§ Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† Ų„ØĨØļØ§ŲØŠ", "partner_page_partner_add_failed": "ŲØ´Ų„ ؁؊ ØĨØļØ§ŲØŠ Ø´ØąŲŠŲƒ", "partner_page_select_partner": "حدد Ø´ØąŲŠŲƒŲ‹Ø§", "partner_page_shared_to_title": "Ų…Ø´ØĒØąŲƒ Ų„", + "partner_page_stop_sharing_content": "{partner} Ų„Ų† ŲŠØšŲˆØ¯ Ų‚Ø§Ø¯ØąØ§ ØšŲ„Ų‰ Ø§Ų„ŲˆØĩ؈؁ Ø§Ų„Ų‰ ØĩŲˆØąŲƒ.", "partner_sharing": "Ų…Ø´Ø§ØąŲƒØŠ Ø§Ų„Ø´ØąŲƒØ§ØĄ", "partners": "Ø§Ų„Ø´ØąŲƒØ§ØĄ", "password": "ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", @@ -1180,14 +1390,16 @@ "permanently_delete_assets_prompt": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ Ø­Ø°Ų {count, plural, one {Ų‡Ø°Ø§ Ø§Ų„ØšŲ†ØĩØąØŸ} other {Ų‡Ø°Ų‡ Ø§Ų„ØšŲ†Ø§ØĩØą #؟}} ØŗŲŠØĒŲ… ØŖŲŠØļŲ‹Ø§ ØĨØ˛Ø§Ų„ØĒŲ‡ {count, plural, one {Ų…Ų† ØŖŲ„Ø¨ŲˆŲ…Ų‡} other {Ų…Ų† ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒŲ‡Ų…}}.", "permanently_deleted_asset": "ØĒŲ… Ø­Ø°Ų Ø§Ų„ØŖØĩŲ„ Ø¨Ø´ŲƒŲ„ Ų†Ų‡Ø§ØĻ؊", "permanently_deleted_assets_count": "ØĒŲ… Ø­Ø°Ų {count, plural, one {# Ų…Ø­ØĒŲˆŲ‰} other {# Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ}} Ų†Ų‡Ø§ØĻŲŠŲ‹Ø§", + "permission": "Ø§Ø°Ų†", + "permission_empty": "Ø§Ų„Ø§Ø°Ų† Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ ؊ØŦب Ø§Ų† Ų„Ø§ ŲŠŲƒŲˆŲ† ŲØ§ØąØēا", "permission_onboarding_back": "ØŽŲ„Ų", "permission_onboarding_continue_anyway": "ØĒŲˆØ§ØĩŲ„ ØšŲ„Ų‰ ØŖŲŠ Ø­Ø§Ų„", "permission_onboarding_get_started": "Ø§Ų„Ø¨Ø¯ØĄ", "permission_onboarding_go_to_settings": "Ø§Ø°Ų‡Ø¨ Ų„Ų„Ø§ØšØ¯Ø§Ø¯Ø§ØĒ", - "permission_onboarding_permission_denied": "ØĒŲ… ØąŲØļ Ø§Ų„ØĨØ°Ų†. Ų„Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ØŒ Ų‚Ų… Ø¨Ų…Ų†Ø­ ØŖØ°ŲˆŲ†Ø§ØĒ Ø§Ų„ØĩŲˆØą ŲˆØ§Ų„ŲŲŠØ¯ŲŠŲˆ ؁؊ Ø§Ų„ØĨؚداداØĒ ", + "permission_onboarding_permission_denied": "ØĒŲ… ØąŲØļ Ø§Ų„ØĨØ°Ų†. Ų„Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ØŒ Ų‚Ų… Ø¨Ų…Ų†Ø­ ØŖØ°ŲˆŲ†Ø§ØĒ Ø§Ų„ØĩŲˆØą ŲˆØ§Ų„ŲŲŠØ¯ŲŠŲˆ ؁؊ Ø§Ų„ØĨؚداداØĒ.", "permission_onboarding_permission_granted": "ØĒŲ… ØĒØŖŲ…ŲŠŲ† Ø§Ų„ØĒØĩØąŲŠØ­! ؈ØļØšŲƒ ØĒŲ…Ø§Ų….", "permission_onboarding_permission_limited": "ØĨØ°Ų† Ų…Ø­Ø¯ŲˆØ¯. Ų„Ų„ØŗŲ…Ø§Ø­ Ø¨Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų„ØĒØˇØ¨ŲŠŲ‚ ؈ØĨØ¯Ø§ØąØŠ Ų…ØŦŲ…ŲˆØšØŠ Ø§Ų„Ų…ØšØąØļ Ø¨Ø§Ų„ŲƒØ§Ų…Ų„ØŒ Ø§Ų…Ų†Ø­ ØŖØ°ŲˆŲ†Ø§ØĒ Ø§Ų„ØĩŲˆØą ŲˆØ§Ų„ŲŲŠØ¯ŲŠŲˆ ؁؊ Ø§Ų„ØĨؚداداØĒ.", - "permission_onboarding_request": "؊ØĒØˇŲ„Ø¨ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ ØĨØ°Ų†Ų‹Ø§ Ų„ØšØąØļ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ", + "permission_onboarding_request": "؊ØĒØˇŲ„Ø¨ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ ØĨØ°Ų†Ų‹Ø§ Ų„ØšØąØļ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ.", "person": "Ø´ØŽØĩ", "person_birthdate": "ØĒØ§ØąŲŠØŽ Ø§Ų„Ų…ŲŠŲ„Ø§Ø¯ {Ø§Ų„ØĒØ§ØąŲŠØŽ}", "person_hidden": "{name}{hidden, select, true { (Ų…ØŽŲŲŠ)} other {}}", @@ -1197,9 +1409,10 @@ "photos_count": "{count, plural, one {{count, number} ØĩŲˆØąØŠ} other {{count, number} ØĩŲˆØą}}", "photos_from_previous_years": "ØĩŲˆØą Ų…Ų† Ø§Ų„ØŗŲ†ŲˆØ§ØĒ Ø§Ų„ØŗØ§Ø¨Ų‚ØŠ", "pick_a_location": "ا؎ØĒØą Ų…ŲˆŲ‚ØšŲ‹Ø§", - "pin_code_changed_successfully": "ØĒŲ… ØĒØēŲŠØą Ø§Ų„ØąŲ‚Ų… Ø§Ų„ØŗØąŲŠ", - "pin_code_reset_successfully": "ØĒŲ… اؚاد؊ ØĒØšŲŠŲŠŲ† Ø§Ų„ØąŲ‚Ų… Ø§Ų„ØŗØąŲŠ", - "pin_code_setup_successfully": "ØĒŲ… Ø§Ų†Ø´Ø§ØĄ ØąŲ‚Ų… ØŗØąŲŠ", + "pin_code_changed_successfully": "ØĒŲ… ØĒØēŲŠØą ØąŲ…Ø˛ PIN Ø¨Ų†ØŦاح", + "pin_code_reset_successfully": "ØĒŲ… اؚاد؊ ØĒØšŲŠŲŠŲ† ØąŲ…Ø˛ PIN Ø¨Ų†ØŦاح", + "pin_code_setup_successfully": "ØĒŲ… Ø§Ų†Ø´Ø§ØĄ ØąŲ…Ø˛ PIN Ø¨Ų†ØŦاح", + "pin_verification": "Ø§Ų„ØĒØ­Ų‚Ų‚ Ø¨ØąŲ…Ø˛ PIN", "place": "Ų…ŲƒØ§Ų†", "places": "Ø§Ų„ØŖŲ…Ø§ŲƒŲ†", "places_count": "{count, plural, one {{count, number} Ų…ŲƒØ§Ų†} other {{count, number} ØŖŲ…Ø§ŲƒŲ†}}", @@ -1207,15 +1420,21 @@ "play_memories": "ØĒØ´ØēŲŠŲ„ Ø§Ų„Ø°ŲƒØąŲŠØ§ØĒ", "play_motion_photo": "ØĒØ´ØēŲŠŲ„ Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĒØ­ØąŲƒØŠ", "play_or_pause_video": "ØĒØ´ØēŲŠŲ„ Ø§Ų„ŲŲŠØ¯ŲŠŲˆ ØŖŲˆ ØĨŲŠŲ‚Ø§ŲŲ‡ Ų…Ø¤Ų‚ØĒŲ‹Ø§", + "please_auth_to_access": "Ø§Ų„ØąØŦØ§ØĄ Ø§Ų„Ų‚ŲŠØ§Ų… Ø¨Ø§Ų„Ų…ØĩØ§Ø¯Ų‚ØŠ Ų„Ų„ŲˆØĩŲˆŲ„", "port": "Ø§Ų„Ų…Ų†ŲØ°", + "preferences_settings_subtitle": "Ø§Ø¯Ø§ØąØŠ ØĒ؁ØļŲŠŲ„Ø§ØĒ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚", "preferences_settings_title": "Ø§Ų„ØĒ؁ØļŲŠŲ„Ø§ØĒ", "preset": "Ø§Ų„ØĨؚداد Ø§Ų„Ų…ØŗØ¨Ų‚", "preview": "Ų…ØšØ§ŲŠŲ†ØŠ", "previous": "Ø§Ų„ØŗØ§Ø¨Ų‚", "previous_memory": "Ø§Ų„Ø°ŲƒØąŲ‰ Ø§Ų„ØŗØ§Ø¨Ų‚ØŠ", - "previous_or_next_photo": "Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„ØŗØ§Ø¨Ų‚ØŠ ØŖŲˆ Ø§Ų„ØĒØ§Ų„ŲŠØŠ", + "previous_or_next_day": "ŲŠŲˆŲ… ØĒØ§Ų„ŲŠ/ØŗØ§Ø¨Ų‚", + "previous_or_next_month": "Ø´Ų‡Øą ØĒØ§Ų„ŲŠ/ØŗØ§Ø¨Ų‚", + "previous_or_next_photo": "ØĩŲˆØąØŠ ØĒØ§Ų„ŲŠØŠ/ØŗØ§Ø¨Ų‚ØŠ", + "previous_or_next_year": "ØŗŲ†ØŠ ØĒØ§Ų„ŲŠØŠ/ØŗØ§Ø¨Ų‚ØŠ", "primary": "ØŖØŗØ§ØŗŲŠ", "privacy": "Ø§Ų„ØŽØĩ؈ØĩŲŠØŠ", + "profile": "Ø­ØŗØ§Ø¨ ØĒØšØąŲŠŲŲŠ", "profile_drawer_app_logs": "Ø§Ų„ØŗØŦŲ„Ø§ØĒ", "profile_drawer_client_out_of_date_major": "ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„Ų‡Ø§ØĒ؁ Ø§Ų„Ų…Ø­Ų…ŲˆŲ„ Ų‚Ø¯ŲŠŲ….ŲŠØąØŦŲ‰ Ø§Ų„ØĒØ­Ø¯ŲŠØĢ ØĨŲ„Ų‰ ØŖØ­Ø¯ØĢ ØĨØĩØ¯Ø§Øą ØąØĻŲŠØŗŲŠ.", "profile_drawer_client_out_of_date_minor": "ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„Ų‡Ø§ØĒ؁ Ø§Ų„Ų…Ø­Ų…ŲˆŲ„ Ų‚Ø¯ŲŠŲ….ŲŠØąØŦŲ‰ Ø§Ų„ØĒØ­Ø¯ŲŠØĢ ØĨŲ„Ų‰ ØŖØ­Ø¯ØĢ ØĨØĩØ¯Ø§Øą ØĩØēŲŠØą.", @@ -1247,7 +1466,7 @@ "purchase_lifetime_description": "Ø§Ų„Ø´ØąØ§ØĄ Ų„Ų…Ø¯Ų‰ Ø§Ų„Ø­ŲŠØ§ØŠ", "purchase_option_title": "ØŽŲŠØ§ØąØ§ØĒ Ø§Ų„Ø´ØąØ§ØĄ", "purchase_panel_info_1": "؊ØĒØˇŲ„Ø¨ Ø¨Ų†Ø§ØĄ Immich Ø§Ų„ŲƒØĢŲŠØą Ų…Ų† Ø§Ų„ŲˆŲ‚ØĒ ŲˆØ§Ų„ØŦŲ‡Ø¯ØŒ ŲˆŲ„Ø¯ŲŠŲ†Ø§ Ų…Ų‡Ų†Ø¯ØŗŲˆŲ† ŲŠØšŲ…Ų„ŲˆŲ† Ø¨Ø¯ŲˆØ§Ų… ŲƒØ§Ų…Ų„ Ų„ØŦØšŲ„Ų‡ ØŖŲØļŲ„ Ų…Ø§ ŲŠŲ…ŲƒŲ†. Ų…Ų‡Ų…ØĒŲ†Ø§ Ų‡ŲŠ ØŖŲ† ØĒØĩبح Ø§Ų„Ø¨ØąŲ…ØŦŲŠØ§ØĒ ؅؁ØĒŲˆØ­ØŠ Ø§Ų„Ų…ØĩØ¯Øą ŲˆŲ…Ų…Ø§ØąØŗØ§ØĒ Ø§Ų„ØšŲ…Ų„ Ø§Ų„ØŖØŽŲ„Ø§Ų‚ŲŠØŠ Ų…ØĩØ¯Øą Ø¯ØŽŲ„ Ų…ØŗØĒØ¯Ø§Ų… Ų„Ų„Ų…ØˇŲˆØąŲŠŲ† ؈ØĨŲ†Ø´Ø§ØĄ Ų†Ø¸Ø§Ų… Ø¨ŲŠØĻ؊ ŲŠØ­ØĒØąŲ… Ø§Ų„ØŽØĩ؈ØĩŲŠØŠ Ų…Øš بداØĻŲ„ Ø­Ų‚ŲŠŲ‚ŲŠØŠ Ų„Ų„ØŽØ¯Ų…Ø§ØĒ Ø§Ų„ØŗØ­Ø§Ø¨ŲŠØŠ Ø§Ų„Ø§ØŗØĒØēŲ„Ø§Ų„ŲŠØŠ.", - "purchase_panel_info_2": "Ų†Ø¸ØąŲ‹Ø§ Ų„ØŖŲ†Ų†Ø§ Ų…Ų„ØĒØ˛Ų…ŲˆŲ† Ø¨ØšØ¯Ų… ØĨØļØ§ŲØŠ Ų†Ø¸Ø§Ų… Ø­Ø¸Øą Ø§Ų„Ø§Ø´ØĒØąØ§Ųƒ ØēŲŠØą Ø§Ų„Ų…Ø¯ŲŲˆØšØŒ ؁ØĨŲ† Ų‡Ø°Ø§ Ø§Ų„Ø´ØąØ§ØĄ Ų„Ų† ŲŠŲ…Ų†Ø­Ųƒ ØŖŲŠ Ų…ŲŠØ˛Ø§ØĒ ØĨØļØ§ŲŲŠØŠ ؁؊ Immich. Ų†Ø­Ų† Ų†ØšØĒŲ…Ø¯ ØšŲ„Ų‰ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† Ų…ØĢŲ„Ųƒ Ų„Ø¯ØšŲ… Ø§Ų„ØĒØˇŲˆŲŠØą Ø§Ų„Ų…ØŗØĒŲ…Øą Ų„Ų€ Immich.", + "purchase_panel_info_2": "Ų†Ø¸ØąŲ‹Ø§ Ų„ØŖŲ†Ų†Ø§ Ų…Ų„ØĒØ˛Ų…ŲˆŲ† Ø¨ØšØ¯Ų… ØĨØļØ§ŲØŠ حاØŦØ˛ Ø¯ŲØšØŒ ؁ØĨŲ† Ų‡Ø°Ø§ Ø§Ų„Ø´ØąØ§ØĄ Ų„Ų† ŲŠŲ…Ų†Ø­Ųƒ ØŖŲŠ Ų…ŲŠØ˛Ø§ØĒ ØĨØļØ§ŲŲŠØŠ ؁؊ Immich. Ų†Ø­Ų† Ų†ØšØĒŲ…Ø¯ ØšŲ„Ų‰ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† Ų…ØĢŲ„Ųƒ Ų„Ø¯ØšŲ… Ø§Ų„ØĒØˇŲˆŲŠØą Ø§Ų„Ų…ØŗØĒŲ…Øą Ų„Ų€ Immich.", "purchase_panel_title": "Ø§Ø¯ØšŲ… Ø§Ų„Ų…Ø´ØąŲˆØš", "purchase_per_server": "Ų„ŲƒŲ„ ØŽØ§Ø¯Ų…", "purchase_per_user": "Ų„ŲƒŲ„ Ų…ØŗØĒØŽØ¯Ų…", @@ -1272,13 +1491,16 @@ "recent": "Ø­Ø¯ŲŠØĢ", "recent-albums": "ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ø§Ų„Ø­Ø¯ŲŠØĢØŠ", "recent_searches": "ØšŲ…Ų„ŲŠØ§ØĒ Ø§Ų„Ø¨Ø­ØĢ Ø§Ų„ØŖØŽŲŠØąØŠ", + "recently_added": "اØļ؊؁ Ų…Ø¤ØŽØąØ§", "recently_added_page_title": "ØŖØļ؊؁ Ų…Ø¤ØŽØąØ§", + "recently_taken": "ØĒŲ… Ø§Ų„ØĒŲ‚Ø§ØˇŲ‡Ø§ Ų…Ø¤ØŽØąŲ‹Ø§", + "recently_taken_page_title": "ØĒŲ… Ø§Ų„ØĒŲ‚Ø§ØˇŲ‡Ø§ Ų…Ø¤ØŽØąŲ‹Ø§", "refresh": "ØĒØ­Ø¯ŲŠØĢ", "refresh_encoded_videos": "ØĒØ­Ø¯ŲŠØĢ Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…Ø´ŲØąØŠ", "refresh_faces": "ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ŲˆØŦŲˆŲ‡", "refresh_metadata": "ØĒØ­Ø¯ŲŠØĢ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ Ø§Ų„ŲˆØĩŲŲŠØŠ", "refresh_thumbnails": "ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĩØēØąØŠ", - "refreshed": "ØĒŲ… Ø§Ų„ØĒØ­Ø¯ŲŠØĢ", + "refreshed": "اؚاد؊ ØĒØ­Ų…ŲŠŲ„", "refreshes_every_file": "ØĨؚاد؊ Ų‚ØąØ§ØĄØŠ ŲƒØ§ŲØŠ Ø§Ų„Ų…Ų„ŲØ§ØĒ Ø§Ų„Ų…ŲˆØŦŲˆØ¯ØŠ ŲˆØ§Ų„ØŦØ¯ŲŠØ¯ØŠ", "refreshing_encoded_video": "ØŦØ§ØąŲ ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…ØąŲ…Ø˛", "refreshing_faces": "ØŦØ§ØąŲŠ ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ŲˆØŦŲˆŲ‡", @@ -1292,12 +1514,16 @@ "remove_deleted_assets": "ØĨØ˛Ø§Ų„ØŠ Ø§Ų„Ų…Ų„ŲØ§ØĒ Ø§Ų„ØēŲŠØą Ų…ØĒØĩŲ„ØŠ", "remove_from_album": "ØĨØ˛Ø§Ų„ØŠ Ų…Ų† Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "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_api_key": "ØĒŲ… ØĨØ˛Ø§ØŠ ؅؁ØĒاح API: â€Ēâ€Ģ{name}", "removed_from_archive": "ØĒŲ…ØĒ ØĨØ˛Ø§Ų„ØĒŲ‡Ø§ Ų…Ų† Ø§Ų„ØŖØąØ´ŲŠŲ", "removed_from_favorites": "ØĒŲ…ØĒ Ø§Ų„ØĨØ˛Ø§Ų„ØŠ Ų…Ų† Ø§Ų„Ų…ŲØļŲ„ØŠ", "removed_from_favorites_count": "{count, plural, other {ØŖŲØ˛ŲŠŲ„ØĒ #}} Ų…Ų† Ø§Ų„ØĒ؁ØļŲŠŲ„Ø§ØĒ", @@ -1315,6 +1541,7 @@ "reset": "ØĨؚاد؊ ØļØ¨Øˇ", "reset_password": "ØĨؚاد؊ ØĒØšŲŠŲŠŲ† ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", "reset_people_visibility": "ØĨؚاد؊ ØļØ¨Øˇ Ø¸Ų‡ŲˆØą Ø§Ų„ØŖØ´ØŽØ§Øĩ", + "reset_pin_code": "اؚاد؊ ØĒØšŲŠŲŠŲ† ØąŲ…Ø˛ PIN", "reset_to_default": "ØĨؚاد؊ Ø§Ų„ØĒØšŲŠŲŠŲ† ØĨŲ„Ų‰ Ø§Ų„Ø§ŲØĒØąØ§Øļ؊", "resolve_duplicates": "Ų…ØšØ§Ų„ØŦØŠ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ų…ŲƒØąØąØŠ", "resolved_all_duplicates": "ØĒŲ… Ø­Ų„ ØŦŲ…ŲŠØš Ø§Ų„ØĒŲƒØąØ§ØąØ§ØĒ", @@ -1329,6 +1556,7 @@ "role_editor": "Ø§Ų„Ų…Ø­ØąØą", "role_viewer": "Ø§Ų„ØšØ§ØąØļ", "save": "Ø­ŲØ¸", + "save_to_gallery": "Ø­ŲØ¸ Ø§Ų„Ų‰ Ø§Ų„Ų…ØšØąØļ", "saved_api_key": "ØĒŲ… Ø­ŲØ¸ ؅؁ØĒاح Ø§Ų„Ų€ API", "saved_profile": "ØĒŲ… Ø­ŲØ¸ Ø§Ų„Ų…Ų„Ų", "saved_settings": "ØĒŲ… Ø­ŲØ¸ Ø§Ų„ØĨؚداداØĒ", @@ -1349,19 +1577,33 @@ "search_camera_model": "Ø§Ų„Ø¨Ø­ØĢ Ø­ØŗØ¨ Ų…ŲˆØ¯ŲŠŲ„ Ø§Ų„ŲƒØ§Ų…ŲŠØąØ§...", "search_city": "Ø§Ų„Ø¨Ø­ØĢ Ø­ØŗØ¨ Ø§Ų„Ų…Ø¯ŲŠŲ†ØŠ...", "search_country": "Ø§Ų„Ø¨Ø­ØĢ Ø­ØŗØ¨ Ø§Ų„Ø¯ŲˆŲ„ØŠ...", - "search_filter_apply": "ا؎ØĒØ§Øą Ø§Ų„ŲŲ„ØĒØą ", + "search_filter_apply": "ا؎ØĒØ§Øą Ø§Ų„ŲŲ„ØĒØą", + "search_filter_camera_title": "ا؎ØĒØą Ų†ŲˆØš Ø§Ų„ŲƒØ§Ų…ŲŠØąØ§", + "search_filter_date": "ØĒØ§ØąŲŠØŽ", + "search_filter_date_interval": "{start} Ø§Ų„Ų‰ {end}", + "search_filter_date_title": "حدد Ų†ØˇØ§Ų‚ Ø§Ų„ØĒØ§ØąŲŠØŽ", "search_filter_display_option_not_in_album": "Ų„ŲŠØŗ ؁؊ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", + "search_filter_display_options": "ØŽŲŠØ§ØąØ§ØĒ Ø§Ų„ØšØąØļ", + "search_filter_filename": "بحØĢ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„Ø§ØŗŲ…", + "search_filter_location": "Ø§Ų„Ų…ŲˆŲ‚Øš", + "search_filter_location_title": "ا؎ØĒØą Ø§Ų„Ų…ŲˆŲ‚Øš", + "search_filter_media_type": "Ų†ŲˆØš Ø§Ų„ŲˆØŗØ§ØĻØˇ", + "search_filter_media_type_title": "ا؎ØĒØą Ų†ŲˆØš Ø§Ų„ŲˆØŗØ§ØĻØˇ", + "search_filter_people_title": "ا؎ØĒØą Ø§Ų„Ø§Ø´ØŽØ§Øĩ", "search_for": "Ø§Ų„Ø¨Ø­ØĢ ØšŲ†", "search_for_existing_person": "Ø§Ų„Ø¨Ø­ØĢ ØšŲ† Ø´ØŽØĩ Ų…ŲˆØŦŲˆØ¯", + "search_no_more_result": "Ų„Ø§ ØĒ؈ØŦد Ų†ØĒاØĻØŦ اØļØ§ŲŲŠØŠ", "search_no_people": "Ų„Ø§ ؊؈ØŦد ØŖØ´ØŽØ§Øĩ", "search_no_people_named": "Ų„Ø§ ؊؈ØŦد ØŖØ´ØŽØ§Øĩ Ø¨Ø§Ų„Ø§ØŗŲ… \"{name}\"", + "search_no_result": "Ų„Ø§ ØĒ؈ØŦد Ų†ØĒاØĻØŦ، Ø­Ø§ŲˆŲ„ Ø§ØŗØĒØŽØ¯Ø§Ų… Ų…ØĩØˇŲ„Ø­ بحØĢ Ų…ØŽØĒ؄؁ Ø§Ųˆ ØĒØąŲƒŲŠØ¨ØŠ", "search_options": "ØŽŲŠØ§ØąØ§ØĒ Ø§Ų„Ø¨Ø­ØĢ", "search_page_categories": "؁ØĻاØĒ", "search_page_motion_photos": "Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĒØ­ØąŲƒŲ‡", "search_page_no_objects": "Ų„Ø§ ØĒ؈ØŦد Ų…ØšŲ„ŲˆŲ…Ø§ØĒ ØšŲ† ØŖØ´ŲŠØ§ØĄ Ų…ØĒاح؊", "search_page_no_places": "Ų„Ø§ ØĒ؈ØŦد Ų…ØšŲ„ŲˆŲ…Ø§ØĒ Ų…ØĒŲˆŲØąØŠ Ų„Ų„ØŖŲ…Ø§ŲƒŲ†", "search_page_screenshots": "Ų„Ų‚ØˇØ§ØĒ Ø§Ų„Ø´Ø§Ø´ØŠ", - "search_page_selfies": " ØĩŲˆØą ذاØĒŲŠŲ‡", + "search_page_search_photos_videos": "ابحØĢ ØšŲ† ØĩŲˆØąŲƒ Ø§Ųˆ ŲØ¯ŲŠŲˆŲ‡Ø§ØĒ؃", + "search_page_selfies": "ØĩŲˆØą ذاØĒŲŠŲ‡", "search_page_things": "ØŖØ´ŲŠØ§ØĄ", "search_page_view_all_button": "ØšØąØļ Ø§Ų„ŲƒŲ„", "search_page_your_activity": "Ų†Ø´Ø§ØˇŲƒ", @@ -1372,7 +1614,7 @@ "search_result_page_new_search_hint": "بحØĢ ØŦØ¯ŲŠØ¯", "search_settings": "ØĨؚداداØĒ Ø§Ų„Ø¨Ø­ØĢ", "search_state": "Ø§Ų„Ø¨Ø­ØĢ Ø­ØŗØ¨ Ø§Ų„ŲˆŲ„Ø§ŲŠØŠ...", - "search_suggestion_list_smart_search_hint_1": "؊ØĒŲ… ØĒŲ…ŲƒŲŠŲ† Ø§Ų„Ø¨Ø­ØĢ Ø§Ų„Ø°ŲƒŲŠ Ø§ŲØĒØąØ§ØļŲŠŲ‹Ø§ ، Ų„Ų„Ø¨Ø­ØĢ ØšŲ† Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ Ø§Ų„ŲˆØĩŲŲŠØŠ ، Ø§ØŗØĒØŽØ¯Ų… Ø¨Ų†Ø§ØĄ Ø§Ų„ØŦŲ…Ų„ØŠ", + "search_suggestion_list_smart_search_hint_1": "؊ØĒŲ… ØĒŲ…ŲƒŲŠŲ† Ø§Ų„Ø¨Ø­ØĢ Ø§Ų„Ø°ŲƒŲŠ Ø§ŲØĒØąØ§ØļŲŠŲ‹Ø§ ، Ų„Ų„Ø¨Ø­ØĢ ØšŲ† Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ Ø§Ų„ŲˆØĩŲŲŠØŠ ، Ø§ØŗØĒØŽØ¯Ų… Ø¨Ų†Ø§ØĄ Ø§Ų„ØŦŲ…Ų„ØŠ. ", "search_suggestion_list_smart_search_hint_2": "Ų…: Ø§Ų„Ø¨Ø­ØĢ Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ", "search_tags": "Ø§Ų„Ø¨Ø­ØĢ ØšŲ† Ø§Ų„ØšŲ„Ø§Ų…Ø§ØĒ...", "search_timezone": "Ø§Ų„Ø¨Ø­ØĢ Ø­ØŗØ¨ Ø§Ų„Ų…Ų†ØˇŲ‚ØŠ Ø§Ų„Ø˛Ų…Ų†ŲŠØŠ...", @@ -1385,6 +1627,7 @@ "select_album_cover": "ØĒØ­Ø¯ŲŠØ¯ ØēŲ„Ø§Ų Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "select_all": "ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ŲƒŲ„", "select_all_duplicates": "ØĒØ­Ø¯ŲŠØ¯ ØŦŲ…ŲŠØš Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ų…ŲƒØąØąØŠ", + "select_all_in": "ا؎ØĒØą Ø§Ų„ŲƒŲ„ ؁؊ {group}", "select_avatar_color": "ØĒØ­Ø¯ŲŠØ¯ Ų„ŲˆŲ† Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„Ø´ØŽØĩŲŠØŠ", "select_face": "ØĒØ­Ø¯ŲŠØ¯ ؈ØŦŲ‡", "select_featured_photo": "ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„Ų…Ų…ŲŠØ˛ØŠ", @@ -1392,6 +1635,7 @@ "select_keep_all": "ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ØŖØ­ØĒŲØ§Ø¸ Ø¨Ø§Ų„ŲƒŲ„", "select_library_owner": "ØĒØ­Ø¯ŲŠØ¯ Ų…Ø§Ų„ŲŲƒ Ø§Ų„Ų…ŲƒØĒب؊", "select_new_face": "ØĒØ­Ø¯ŲŠØ¯ ؈ØŦŲ‡ ØŦØ¯ŲŠØ¯", + "select_person_to_tag": "ا؎ØĒØą Ø´ØŽØĩ Ų„ŲˆØļØš ØšŲ„Ø§Ų…ØŠ", "select_photos": "ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ØĩŲˆØą", "select_trash_all": "ØĒØ­Ø¯ŲŠØ¯ Ø­Ø°Ų Ø§Ų„ŲƒŲ„Ų", "select_user_for_sharing_page_err_album": "ŲØ´Ų„ ؁؊ ØĨŲ†Ø´Ø§ØĄ ØŖŲ„Ø¨ŲˆŲ…", @@ -1399,10 +1643,12 @@ "selected_count": "{count, plural, other {# Ų…Ø­Ø¯Ø¯ØŠ }}", "send_message": "‏ØĨØąØŗØ§Ų„ ØąØŗØ§Ų„ØŠ", "send_welcome_email": "ØĨØąØŗØ§Ų„ Ø¨ØąŲŠØ¯Ų‹Ø§ ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠŲ‹Ø§ ØĒØąØ­ŲŠØ¨ŲŠŲ‹Ø§", + "server_endpoint": "Ų†Ų‚ØˇØŠ Ų†Ų‡Ø§ŲŠØŠ Ø§Ų„ØŽØ§Ø¯Ų…", "server_info_box_app_version": "Ų†ØŗØŽØŠ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚", "server_info_box_server_url": "ØšŲ†ŲˆØ§Ų† URL Ø§Ų„ØŽØ§Ø¯Ų…", "server_offline": "Ø§Ų„ØŽØ§Ø¯Ų… ØēŲŠØą Ų…ØĒØĩŲ„", "server_online": "Ø§Ų„ØŽØ§Ø¯Ų… Ų…ØĒØĩŲ„", + "server_privacy": "ØŽØĩ؈ØĩŲŠØŠ Ø§Ų„ØŽØ§Ø¯Ų…", "server_stats": "ØĨØ­ØĩاØĻŲŠØ§ØĒ Ø§Ų„ØŽØ§Ø¯Ų…", "server_version": "ØĨØĩØ¯Ø§Øą Ø§Ų„ØŽØ§Ø¯Ų…", "set": "‏ØĒØ­Ø¯ŲŠØ¯", @@ -1412,6 +1658,7 @@ "set_date_of_birth": "ØĒØ­Ø¯ŲŠØ¯ ØĒØ§ØąŲŠØŽ Ø§Ų„Ų…ŲŠŲ„Ø§Ø¯", "set_profile_picture": "ØĒØ­Ø¯ŲŠØ¯ ØĩŲˆØąØŠ Ø§Ų„Ų…Ų„Ų Ø§Ų„Ø´ØŽØĩ؊", "set_slideshow_to_fullscreen": "ØĒØ­Ø¯ŲŠØ¯ ØšØąØļ Ø§Ų„Ø´ØąØ§ØĻØ­ ØšŲ„Ų‰ ؈ØļØš Ų…Ų„ØĄ Ø§Ų„Ø´Ø§Ø´ØŠ", + "set_stack_primary_asset": "ØĒØšŲŠŲŠŲ† ŲƒØŖØĩŲ„ Ø§ØŗØ§ØŗŲŠ", "setting_image_viewer_help": "ŲŠŲ‚ŲˆŲ… ØšØ§ØąØļ Ø§Ų„ØĒŲØ§ØĩŲŠŲ„ بØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„Ų…ØĩØēØąØŠ Ø§Ų„ØĩØēŲŠØąØŠ ØŖŲˆŲ„Ø§Ų‹ ، ØĢŲ… ŲŠŲ‚ŲˆŲ… بØĒØ­Ų…ŲŠŲ„ Ø§Ų„Ų…ØšØ§ŲŠŲ†ØŠ Ų…ØĒŲˆØŗØˇØŠ Ø§Ų„Ø­ØŦŲ… (ØĨذا ØĒŲ… ØĒŲ…ŲƒŲŠŲ†Ų‡Ø§) ، ŲˆŲŠŲ‚ŲˆŲ… ØŖØŽŲŠØąŲ‹Ø§ بØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØŖØĩŲ„ (ØĨذا ØĒŲ… ØĒŲ…ŲƒŲŠŲ†Ų‡).", "setting_image_viewer_original_subtitle": "ØĒŲ…ŲƒŲŠŲ† ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„ŲƒØ§Ų…Ų„ØŠ Ø§Ų„Ø¯Ų‚ØŠ Ø§Ų„ØŖØĩŲ„ŲŠØŠ (ŲƒØ¨ŲŠØąØŠ!).ØĒØšØˇŲŠŲ„ Ų„ØĒŲ‚Ų„ŲŠŲ„ Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ (ŲƒŲ„ Ų…Ų† Ø§Ų„Ø´Ø¨ŲƒØŠ ŲˆØšŲ„Ų‰ Ø°Ø§ŲƒØąØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø¤Ų‚ØĒ Ų„Ų„ØŦŲ‡Ø§Ø˛).", "setting_image_viewer_original_title": "ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„ØŖØĩŲ„ŲŠØŠ", @@ -1419,21 +1666,30 @@ "setting_image_viewer_preview_title": "ØĒØ­Ų…ŲŠŲ„ ØĩŲˆØąØŠ Ų…ØšØ§ŲŠŲ†ØŠ", "setting_image_viewer_title": "Ø§Ų„ØĩŲˆØą", "setting_languages_apply": "ØĒØēŲŠŲŠØą Ø§Ų„ØĨؚداداØĒ", + "setting_languages_subtitle": "ØĒØēŲŠŲŠØą Ų„ØēØŠ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚", + "setting_notifications_notify_failures_grace_period": "Ø§Ų„ØĒŲ†Ø¨ŲŠŲ‡ Ø¨ŲØ´Ų„ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ ؁؊ Ø§Ų„ØŽŲ„ŲŲŠØŠ: {duration}", + "setting_notifications_notify_hours": "{count} ØŗØ§ØšØ§ØĒ", "setting_notifications_notify_immediately": "؁؊ Ø§Ų„Ø­Ø§Ų„", + "setting_notifications_notify_minutes": "{count} Ø¯Ų‚Ø§ØĻŲ‚", "setting_notifications_notify_never": "ØŖØ¨Ø¯Ø§Ų‹", + "setting_notifications_notify_seconds": "{count} ØĢŲˆØ§Ų†ŲŠ", "setting_notifications_single_progress_subtitle": "Ų…ØšŲ„ŲˆŲ…Ø§ØĒ Ø§Ų„ØĒŲ‚Ø¯Ų… Ø§Ų„ØĒ؁ØĩŲŠŲ„ŲŠØŠ ØĒØ­Ų…ŲŠŲ„ Ų„ŲƒŲ„ ØŖØĩŲ„", "setting_notifications_single_progress_title": "ØĨØ¸Ų‡Ø§Øą ØĒŲ‚Ø¯Ų… Ø§Ų„ØĒŲØ§ØĩŲŠŲ„ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠØŠ Ø§Ų„ØŽŲ„ŲŲŠØŠ", "setting_notifications_subtitle": "اØļØ¨Øˇ ØĒ؁ØļŲŠŲ„Ø§ØĒ Ø§Ų„ØĨØŽØˇØ§Øą", "setting_notifications_total_progress_subtitle": "Ø§Ų„ØĒŲ‚Ø¯Ų… Ø§Ų„ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØšØ§Ų… (ØĒŲ… Ø§Ų„Ų‚ŲŠØ§Ų… Ø¨Ų‡/ØĨØŦŲ…Ø§Ų„ŲŠ Ø§Ų„ØŖØĩŲˆŲ„)", "setting_notifications_total_progress_title": "ØĨØ¸Ų‡Ø§Øą Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ø§Ų„ØŽŲ„ŲŲŠØŠ Ø§Ų„ØĒŲ‚Ø¯Ų… Ø§Ų„Ų…Ø­ØąØ˛", "setting_video_viewer_looping_title": "ØĒŲƒØąØ§Øą Ų…Ų‚ØˇØš ŲŲŠØ¯ŲŠŲˆ ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§", + "setting_video_viewer_original_video_subtitle": "ØšŲ†Ø¯ بØĢ ŲŲŠØ¯ŲŠŲˆ Ų…Ų† Ø§Ų„ØŽØ§Ø¯Ų…ØŒ Ø´ØēŲ‘Ų„ Ø§Ų„Ų†ØŗØŽØŠ Ø§Ų„ØŖØĩŲ„ŲŠØŠ Ø­ØĒŲ‰ Ų…Øš ØĒŲˆŲØą ØĒØąŲ…ŲŠØ˛ Ø¨Ø¯ŲŠŲ„. Ų‚Ø¯ ŲŠØ¤Ø¯ŲŠ Ø°Ų„Ųƒ ØĨŲ„Ų‰ ØĒŲ‚ØˇŲŠØš اØĢŲ†Ø§ØĄ Ø§Ų„ØšØąØļ . ØĒŲØ´ØēŲ‘Ų„ Ø§Ų„ŲŲŠØ¯ŲŠŲˆŲ‡Ø§ØĒ Ø§Ų„Ų…ØĒŲˆŲØąØŠ Ų…Ø­Ų„ŲŠŲ‹Ø§ بØŦŲˆØ¯ØŠ ØŖØĩŲ„ŲŠØŠ بØēØļ Ø§Ų„Ų†Ø¸Øą ØšŲ† Ų‡Ø°Ø§ Ø§Ų„ØĨؚداد.", + "setting_video_viewer_original_video_title": "اØŦØ¨Ø§Øą ØšØąØļ Ø§Ų„ŲØ¯ŲŠŲˆ Ø§Ų„Ø§ØĩŲ„ŲŠ", "settings": "Ø§Ų„ØĨؚداداØĒ", "settings_require_restart": "ŲŠØąØŦŲ‰ ØĨؚاد؊ ØĒØ´ØēŲŠŲ„ Ų„ØĒØˇØ¨ŲŠŲ‚ Ų‡Ø°Ø§ Ø§Ų„ØĨؚداد", "settings_saved": "ØĒŲ… Ø­ŲØ¸ Ø§Ų„ØĨؚداداØĒ", - "setup_pin_code": "ØĒØ­Ø¯ŲŠØ¯ ØąŲ‚Ų… ØŗØąŲŠ", + "setup_pin_code": "ØĒØ­Ø¯ŲŠØ¯ ØąŲ…Ø˛ PIN", "share": "Ų…Ø´Ø§ØąŲƒØŠ", "share_add_photos": "ØĨØļØ§ŲØŠ Ø§Ų„ØĩŲˆØą", + "share_assets_selected": "ا؎ØĒŲŠØ§Øą {count}", "share_dialog_preparing": "ØĒØ­ØļŲŠØą...", + "share_link": "Ų…Ø´Ø§ØąŲƒØŠ ØąØ§Ø¨Øˇ", "shared": "Ų…ŲØ´ØĒŲŽØąŲƒ", "shared_album_activities_input_disable": "Ø§Ų„ØĒØšŲ„ŲŠŲ‚ Ų…ØšØˇŲ„", "shared_album_activity_remove_content": "Ų‡Ų„ ØĒØąŲŠØ¯ Ø­Ø°Ų Ų‡Ø°Ø§ Ø§Ų„Ų†Ø´Ø§ØˇØŸ", @@ -1446,22 +1702,40 @@ "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_edit_description_hint": "ØŖØ¯ØŽŲ„ ؈Øĩ؁ Ø§Ų„Ų…Ø´Ø§ØąŲƒØŠ", "shared_link_edit_expire_after_option_day": "ŲŠŲˆŲ… 1", + "shared_link_edit_expire_after_option_days": "{count} Ø§ŲŠØ§Ų…", "shared_link_edit_expire_after_option_hour": "1 ØŗØ§ØšØŠ", + "shared_link_edit_expire_after_option_hours": "{count} ØŗØ§ØšØ§ØĒ", "shared_link_edit_expire_after_option_minute": "1 Ø¯Ų‚ŲŠŲ‚ØŠ", + "shared_link_edit_expire_after_option_minutes": "{count} Ø¯Ų‚Ø§ØĻŲ‚", + "shared_link_edit_expire_after_option_months": "{count} Ø§Ø´Ų‡Øą", + "shared_link_edit_expire_after_option_year": "{count} ØŗŲ†ØŠ", "shared_link_edit_password_hint": "ØŖØ¯ØŽŲ„ ŲƒŲ„Ų…ØŠ Ų…ØąŲˆØą Ø§Ų„Ų…Ø´Ø§ØąŲƒØŠ", "shared_link_edit_submit_button": "ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ØąØ§Ø¨Øˇ", "shared_link_error_server_url_fetch": "Ų„Ø§ ŲŠŲ…ŲƒŲ† ØŦŲ„Ø¨ ØšŲ†ŲˆØ§Ų† Ø§Ų„ØŽØ§Ø¯Ų…", + "shared_link_expires_day": "ØĒŲ†ØĒŲ‡ŲŠ ØĩŲ„Ø§Ø­ŲŠØĒŲ‡ ؁؊ {count} ŲŠŲˆŲ…", + "shared_link_expires_days": "ØĒŲ†ØĒŲ‡ŲŠ ØĩŲ„Ø§Ø­ŲŠØĒŲ‡ ؁؊ {count} Ø§ŲŠØ§Ų…", + "shared_link_expires_hour": "ØĒŲ†ØĒŲ‡ŲŠ ØĩŲ„Ø§Ø­ŲŠØŠ ؁؊ {count} ØŗØ§ØšØŠ", + "shared_link_expires_hours": "ØĒŲ†ØĒŲ‡ŲŠ ØĩŲ„Ø§Ø­ŲŠØĒŲ‡ ؁؊ {count} ØŗØ§ØšØ§ØĒ", + "shared_link_expires_minute": "ØĒŲ†ØĒŲ‡ŲŠ ØĩŲ„Ø§Ø­ŲŠØĒŲ‡ ؁؊ {count} Ø¯Ų‚ŲŠŲ‚ØŠ", + "shared_link_expires_minutes": "ØĒŲ†ØĒŲ‡ŲŠ ØĩŲ„Ø§Ø­ŲŠØĒŲ‡ ؁؊ {count} Ø¯Ų‚Ø§ØĻŲ‚", "shared_link_expires_never": "ØĒŲ†ØĒŲ‡ŲŠ ∞", + "shared_link_expires_second": "ØĒŲ†ØĒŲ‡ŲŠ ØĩŲ„Ø§Ø­ŲŠØĒŲ‡ ؁؊ {count} ØĢØ§Ų†ŲŠØŠ", + "shared_link_expires_seconds": "ØĒŲ†ØĒŲ‡ŲŠ ØĩŲ„Ø§Ø­ŲŠØĒŲ‡ ؁؊ {count} ØĢŲˆØ§Ų†ŲŠ", + "shared_link_individual_shared": "Ų…Ø´Ø§ØąŲƒØŠ ŲØąØ¯ŲŠØŠ", + "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØąŲˆØ§Ø¨Øˇ Ø§Ų„Ų…Ø´ØĒØąŲƒØŠ", "shared_link_options": "ØŽŲŠØ§ØąØ§ØĒ Ø§Ų„ØąØ§Ø¨Øˇ Ø§Ų„Ų…Ø´ØĒØąŲƒ", "shared_links": "ØąŲˆØ§Ø¨Øˇ Ų…Ø´ØĒØąŲƒØŠ", "shared_links_description": "؈Øĩ؁ Ø§Ų„ØąŲˆØ§Ø¨Øˇ Ø§Ų„Ų…Ø´ØĒØąŲƒØŠ", "shared_photos_and_videos_count": "{assetCount, plural, other {# Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…ŲØ´Ø§ØąŲŽŲƒØŠ.}}", + "shared_with_me": "ØĒŲ…ØĒ Ų…Ø´Ø§ØąŲƒØĒŲ‡Ø§ Ų…ØšŲŠ", "shared_with_partner": "ØĒŲ…ØĒ Ø§Ų„Ų…Ø´Ø§ØąŲƒØŠ Ų…Øš {partner}", "sharing": "Ų…Ø´Ø§ØąŲƒØŠ", "sharing_enter_password": "Ø§Ų„ØąØŦØ§ØĄ ØĨØ¯ØŽØ§Ų„ ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą Ų„ØšØąØļ Ų‡Ø°Ų‡ Ø§Ų„ØĩŲØ­ØŠ.", @@ -1522,12 +1796,14 @@ "start_date": "ØĒØ§ØąŲŠØŽ Ø§Ų„Ø¨Ø¯ØĄ", "state": "Ø§Ų„ŲˆŲ„Ø§ŲŠØŠ", "status": "Ø§Ų„Ø­Ø§Ų„ØŠ", + "stop_casting": "Ø§ŲŠŲ‚Ø§Ų Ø§Ų„Ø¨ØĢ", "stop_motion_photo": "ØĨŲŠŲ‚Ø§Ų Ø­ØąŲƒØŠ Ø§Ų„ØĩŲˆØąØŠ", "stop_photo_sharing": "ØĒŲˆŲ‚Ų ØšŲ† Ų…Ø´Ø§ØąŲƒØŠ ØĩŲˆØąŲƒØŸ", "stop_photo_sharing_description": "Ų„Ų† ؊ØĒŲ…ŲƒŲ† {partner} Ų…Ų† Ø§Ų„ŲˆØĩŲˆŲ„ ØĨŲ„Ų‰ ØĩŲˆØąŲƒ بؚد Ø§Ų„ØĸŲ†.", "stop_sharing_photos_with_user": "ØĒŲˆŲ‚Ų ØšŲ† Ų…Ø´Ø§ØąŲƒØŠ ØĩŲˆØąŲƒ Ų…Øš Ų‡Ø°Ø§ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", "storage": "Ų…ØŗØ§Ø­ØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ†", - "storage_label": "ØĒØŗŲ…ŲŠØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ†", + "storage_label": "ØŗŲ…ØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ†", + "storage_quota": "Ø­ØĩØŠ Ø§Ų„ØŽØ˛Ų†", "storage_usage": "{used} Ų…Ų† {available} Ų…ŲØŗØĒØŽŲ’Ø¯Ų…", "submit": "ØĨØąØŗØ§Ų„", "suggestions": "Ø§Ų‚ØĒØąØ§Ø­Ø§ØĒ", @@ -1537,6 +1813,9 @@ "support_third_party_description": "ØĒŲ… Ø­Ø˛Ų… ØĒØĢØ¨ŲŠØĒ immich Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ Ø¨ŲˆØ§ØŗØˇØŠ ØŦŲ‡ØŠ ØŽØ§ØąØŦŲŠØŠ. Ų‚Ø¯ ØĒŲƒŲˆŲ† Ø§Ų„Ų…Ø´ŲƒŲ„Ø§ØĒ Ø§Ų„ØĒ؊ ØĒŲˆØ§ØŦŲ‡Ų‡Ø§ Ų†Ø§ØŦŲ…ØŠ ØšŲ† Ų‡Ø°Ų‡ Ø§Ų„Ø­Ø˛Ų…ØŠØŒ Ų„Ø°Ø§ ŲŠØąØŦŲ‰ ØˇØąØ­ Ø§Ų„Ų…Ø´ŲƒŲ„Ø§ØĒ Ų…ØšŲ‡Ų… ؁؊ Ø§Ų„Ų…Ų‚Ø§Ų… Ø§Ų„ØŖŲˆŲ„ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„ØąŲˆØ§Ø¨Øˇ ØŖØ¯Ų†Ø§Ų‡.", "swap_merge_direction": "ØĒØ¨Ø¯ŲŠŲ„ اØĒØŦØ§Ų‡ Ø§Ų„Ø¯Ų…ØŦ", "sync": "Ų…Ø˛Ø§Ų…Ų†ØŠ", + "sync_albums": "Ų…Ø˛Ø§Ų…Ų†ØŠ Ø§Ų„Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ", + "sync_albums_manual_subtitle": "Ų…Ø˛Ø§Ų…Ų†ØŠ ØŦŲ…ŲŠØš Ø§Ų„ŲØ¯ŲŠŲˆŲ‡Ø§ØĒ ŲˆØ§Ų„ØĩŲˆØą Ø§Ų„Ų…ØąŲŲˆØšØŠ Ø§Ų„Ų‰ Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ Ø§Ų„ØŽØ˛Ų† Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ø§Ų„Ų…ØŽØĒØ§ØąØŠ", + "sync_upload_album_setting_subtitle": "Ø§Ų†Ø´ØĻ ؈ Ø§ØąŲØš ØĩŲˆØąŲƒ ؈ ŲØ¯ŲŠŲˆŲ‡Ø§ØĒ؃ Ø§Ų„Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ Ø§Ų„Ų…ØŽØĒØ§ØąØŠ ؁؊ Immich", "tag": "Ø§Ų„ØšŲ„Ø§Ų…ØŠ", "tag_assets": "ØŖØĩŲˆŲ„ Ø§Ų„ØšŲ„Ø§Ų…ØŠ", "tag_created": "ØĒŲ… ØĨŲ†Ø´Ø§ØĄ Ø§Ų„ØšŲ„Ø§Ų…ØŠ: {tag}", @@ -1551,8 +1830,14 @@ "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": "Ų‚Ø¯ ŲŠØ˛ŲŠØ¯ Ø§Ų„ØĒØ­Ų…ŲŠŲ„ Ų…Ų† ØĢŲ„Ø§ØĢ Ų…ØąØ§Ø­Ų„ Ų…Ų† ØŖØ¯Ø§ØĄ Ø§Ų„ØĒØ­Ų…ŲŠŲ„ ŲˆŲ„ŲƒŲ†Ų‡ ŲŠØŗØ¨Ø¨ ØĒØ­Ų…ŲŠŲ„ Ø´Ø¨ŲƒØŠ ØŖØšŲ„Ų‰ Ø¨ŲƒØĢŲŠØą", @@ -1572,22 +1857,29 @@ "total": "Ø§Ų„ØĨØŦŲ…Ø§Ų„ŲŠ", "total_usage": "Ø§Ų„Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„ØĨØŦŲ…Ø§Ų„ŲŠ", "trash": "Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", + "trash_action_prompt": "{count} Ų†Ų‚Ų„ Ø§Ų„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "trash_all": "Ų†Ų‚Ų„ Ø§Ų„ŲƒŲ„ ØĨŲ„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "trash_count": "ØŗŲ„ØŠ Ø§Ų„Ų…Ø­Ų…Ų„Ø§ØĒ {count, number}", "trash_delete_asset": "Ø­Ø°Ų/Ų†Ų‚Ų„ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ ØĨŲ„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", + "trash_emptied": "ØŗØ¨ØŠ Ų…Ų‡Ų…Ų„Ø§ Ų…ŲØąØēØŠ", "trash_no_results_message": "ØŗØĒØ¸Ų‡Øą Ų‡Ų†Ø§ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…Ø­Ø°ŲˆŲØŠ.", "trash_page_delete_all": "Ø­Ø°Ų Ø§Ų„ŲƒŲ„", "trash_page_empty_trash_dialog_content": "Ų‡Ų„ ØĒØąŲŠØ¯ ØĒŲØąŲŠØē ØŖØĩŲˆŲ„Ųƒ Ø§Ų„Ų…Ų‡Ų…Ų„ØŠØŸ ØŗØĒØĒŲ… ØĨØ˛Ø§Ų„ØŠ Ų‡Ø°Ų‡ Ø§Ų„ØšŲ†Ø§ØĩØą Ų†Ų‡Ø§ØĻŲŠŲ‹Ø§ Ų…Ų† Ø§Ų„ØĒØˇØ¨ŲŠŲ‚", + "trash_page_info": "Ø§Ų„ØšŲ†Ø§ØĩØą Ø§Ų„Ų…Ų†Ų‚ŲˆŲ„ØŠ Ø§Ų„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ ØŗŲŠØĒŲ… Ø­Ø°ŲŲ‡Ø§ Ø¨Ø´ŲƒŲ„ Ų†Ų‡Ø§ØĻ؊ بؚد {days} Ø§ŲŠØ§Ų…", "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 {# ŲŠŲˆŲ…} other {# ØŖŲŠØ§Ų… }}.", "type": "Ø§Ų„Ų†ŲˆØš", - "unable_to_change_pin_code": "ØĒŲŲŠŲŠØą Ø§Ų„ØąŲ‚Ų… Ø§Ų„ØŗØąŲŠ ØēŲŠØą Ų…Ų…ŲƒŲ†", - "unable_to_setup_pin_code": "Ø§Ų†Ø´Ø§ØĄ Ø§Ų„ØąŲ‚Ų… Ø§Ų„ØŗØąŲŠ ØēŲŠØą Ų…Ų…ŲƒŲ†", + "unable_to_change_pin_code": "ØĒŲŲŠŲŠØą ØąŲ…Ø˛ PIN ØēŲŠØą Ų…Ų…ŲƒŲ†", + "unable_to_setup_pin_code": "Ø§Ų†Ø´Ø§ØĄ ØąŲ…Ø˛ PIN ØēŲŠØą Ų…Ų…ŲƒŲ†", "unarchive": "ØŖØŽØąØŦ Ų…Ų† Ø§Ų„ØŖØąØ´ŲŠŲ", + "unarchive_action_prompt": "{count} Ø§Ø˛ŲŠŲ„ Ų…Ų† Ø§Ų„Ø§ØąØ´ŲŠŲ", "unarchived_count": "{count, plural, other {ØēŲŠØą Ų…Ø¤ØąØ´ŲØŠ #}}", + "undo": "ØĒØąØ§ØŦØš", "unfavorite": "ØŖØ˛Ų„ Ø§Ų„ØĒ؁ØļŲŠŲ„", + "unfavorite_action_prompt": "{count} Ø§Ø˛ŲŠŲ„ Ų…Ų† Ø§Ų„Ų…ŲØļŲ„Ø§ØĒ", "unhide_person": "ØŖØ¸Ų‡Øą Ø§Ų„Ø´ØŽØĩ", "unknown": "ØēŲŠØą Ų…ØšØąŲˆŲ", "unknown_country": "Ø¨Ų„Ø¯ ØēŲŠØą Ų…ØšØąŲˆŲ", @@ -1603,9 +1895,11 @@ "unsaved_change": "ØĒØēŲŠŲŠØą ØēŲŠØą Ų…Ø­ŲŲˆØ¸", "unselect_all": "ØĨŲ„ØēØ§ØĄ ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ŲƒŲ„", "unselect_all_duplicates": "ØĨŲ„ØēØ§ØĄ ØĒØ­Ø¯ŲŠØ¯ ŲƒØ§ŲØŠ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ų…ŲƒØąØąØŠ", + "unselect_all_in": "ØĨŲ„ØēØ§ØĄ ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ŲƒŲ„ ؁؊ {group}", "unstack": "؁؃ Ø§Ų„ŲƒŲˆŲ…Ų‡", "unstacked_assets_count": "ØĒŲ… ØĨØŽØąØ§ØŦ {count, plural, one {# Ø§Ų„ØŖØĩŲ„} other {# Ø§Ų„ØŖØĩŲˆŲ„}} Ų…Ų† Ø§Ų„ØĒŲƒØ¯ŲŠØŗ", "up_next": "Ø§Ų„ØĒØ§Ų„ŲŠ", + "updated_at": "ØĒŲ… Ø§Ų„ØĒØ­Ø¯ŲŠØĢ", "updated_password": "ØĒŲ… ØĒØ­Ø¯ŲŠØĢ ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", "upload": "ØąŲØš", "upload_concurrency": "Ø§Ų„ØąŲØš Ø§Ų„Ų…ØĒØ˛Ø§Ų…Ų†", @@ -1618,14 +1912,20 @@ "upload_status_errors": "Ø§Ų„ØŖØŽØˇØ§ØĄ", "upload_status_uploaded": "ØĒŲ… Ø§Ų„ØąŲØš", "upload_success": "ØĒŲ… Ø§Ų„ØąŲØš Ø¨Ų†ØŦاح، Ų‚Ų… بØĒØ­Ø¯ŲŠØĢ Ø§Ų„ØĩŲØ­ØŠ Ų„ØąØ¤ŲŠØŠ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„Ų…ØąŲŲˆØšØŠ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ.", + "upload_to_immich": "Ø§Ų„ØąŲØš Ø§Ų„Ų‰Immich ‎ ‏ ({count})", + "uploading": "ØŦØ§ØąŲŠ Ø§Ų„ØąŲØš", "url": "ØšŲ†ŲˆØ§Ų† URL", "usage": "Ø§Ų„Ø§ØŗØĒØŽØ¯Ø§Ų…", + "use_biometric": "Ø§ØŗØĒØŽØ¯Ų… Ø§Ų„Ø¨Ø§ŲŠŲˆŲ…ØĒØąŲŠ", + "use_current_connection": "Ø§ØŗØĒØŽØ¯Ų… Ø§Ų„Ø§ØĒØĩØ§Ų„ Ø§Ų„Ø­Ø§Ų„ŲŠ", "use_custom_date_range": "Ø§ØŗØĒØŽØ¯Ų… Ø§Ų„Ų†ØˇØ§Ų‚ Ø§Ų„Ø˛Ų…Ų†ŲŠ Ø§Ų„Ų…ØŽØĩØĩ Ø¨Ø¯Ų„Ø§Ų‹ Ų…Ų† Ø°Ų„Ųƒ", "user": "Ų…ØŗØĒØŽØ¯Ų…", + "user_has_been_deleted": "Ų‡Ø°Ø§ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų… ØĒŲ… Ø­Ø°ŲŲ‡.", "user_id": "Ų…ØšØąŲ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", "user_liked": "Ų‚Ø§Ų… {user} Ø¨Ø§Ų„ØĨØšØŦاب {type, select, photo {Ø¨Ų‡Ø°Ų‡ Ø§Ų„ØĩŲˆØąØŠ} video {Ø¨Ų‡Ø°Ø§ Ø§Ų„ŲŲŠØ¯ŲŠŲˆ} asset {Ø¨Ų‡Ø°Ø§ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰} other {Ø¨Ų‡Ø§}}", - "user_pin_code_settings": "Ø§Ų„ØąŲ‚Ų… Ø§Ų„ØŗØąŲŠ", - "user_pin_code_settings_description": "ØĒØēŲŠØą Ø§Ų„ØąŲ‚Ų… Ø§Ų„ØŗØąŲŠ", + "user_pin_code_settings": "ØąŲ…Ø˛ PIN", + "user_pin_code_settings_description": "ØĒØēŲŠØą ØąŲ…Ø˛ PIN", + "user_privacy": "ØŽØĩ؈ØĩŲŠØŠ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", "user_purchase_settings": "Ø§Ų„Ø´ØąØ§ØĄ", "user_purchase_settings_description": "ØĨØ¯Ø§ØąØŠ ØšŲ…Ų„ŲŠØŠ Ø§Ų„Ø´ØąØ§ØĄ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ", "user_role_set": "Ų‚Ų… بØĒØšŲŠŲŠŲ† {user} ŲƒŲ€ {role}", @@ -1636,6 +1936,7 @@ "users": "Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ†", "utilities": "ØŖØ¯ŲˆØ§ØĒ", "validate": "ØĒØ­Ų‚Ų’Ų‚", + "validate_endpoint_error": "Ø§Ų„ØąØŦØ§ØĄ Ø§Ø¯ØŽØ§Ų„ ØšŲ†ŲˆØ§Ų† URL ØĩØ§Ų„Ø­", "variables": "Ø§Ų„Ų…ØĒØēŲŠØąØ§ØĒ", "version": "Ø§Ų„ØĨØĩØ¯Ø§Øą", "version_announcement_closing": "ØĩØ¯ŲŠŲ‚ŲƒØŒ ØŖŲ„ŲŠŲƒØŗ", @@ -1657,7 +1958,9 @@ "view_name": "ØšØąØļ", "view_next_asset": "ØšØąØļ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ Ø§Ų„ØĒØ§Ų„ŲŠ", "view_previous_asset": "ØšØąØļ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ Ø§Ų„ØŗØ§Ø¨Ų‚", + "view_qr_code": "Â­ØšØąØļ ØąŲ…Ø˛ Ø§Ų„Ø§ØŗØĒØŦاب؊ Ø§Ų„ØŗØąŲŠØšØŠ", "view_stack": "ØšØąØļ Ø§Ų„ØĒŲƒØ¯ŲŠØŗ", + "view_user": "ØšØąØļ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", "viewer_remove_from_stack": "Ø­Ø°Ų Ų…Ų† Ø§Ų„ŲƒŲˆŲ…Ų‡ ØŖŲˆ Ø§Ų„Ų…ØŦŲ…ŲˆØšØŠ", "viewer_stack_use_as_main_asset": "Ø§ØŗØĒØŽØ¯Ų… ŲƒØŖØĩŲ„ ØąØĻŲŠØŗŲŠ", "viewer_unstack": "؁؃ Ø§Ų„ŲƒŲˆŲ…Ų‡", @@ -1667,11 +1970,12 @@ "week": "ØŖØŗØ¨ŲˆØš", "welcome": "Ų…ØąØ­Ø¨Ø§Ų‹", "welcome_to_immich": "Ų…ØąØ­Ø¨Ø§Ų‹ Ø¨Ųƒ ؁؊ Immich", - "wifi_name": "WiFi Name", + "wifi_name": "Ø§ØŗŲ… Ø´Ø¨ŲƒØŠ Wi-Fi", + "wrong_pin_code": "ØąŲ…Ø˛ PIN ØŽØ§ØˇØĻ", "year": "ØŗŲ†ØŠ", "years_ago": "Ų…Ų†Ø° {years, plural, one {# ØŗŲ†ØŠ} other {# ØŗŲ†ŲˆØ§ØĒ}}", "yes": "Ų†ØšŲ…", "you_dont_have_any_shared_links": "Ų„ŲŠØŗ Ų„Ø¯ŲŠŲƒ ØŖŲŠ ØąŲˆØ§Ø¨Øˇ Ų…Ø´ØĒØąŲƒØŠ", - "your_wifi_name": "Your WiFi name", + "your_wifi_name": "Ø§ØŗŲ… Ø´Ø¨ŲƒØŠ Wi-Fi Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ", "zoom_image": "ØĒŲƒØ¨ŲŠØą Ø§Ų„ØĩŲˆØąØŠ" } diff --git a/i18n/be.json b/i18n/be.json index 470030b6f6..d3f59b32a5 100644 --- a/i18n/be.json +++ b/i18n/be.json @@ -22,6 +22,7 @@ "add_partner": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ĐŋĐ°Ņ€Ņ‚ĐŊŅ‘Ņ€Đ°", "add_path": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ҈ĐģŅŅ…", "add_photos": "Đ”Đ°Đ´Đ°Ņ†ŅŒ Ņ„ĐžŅ‚Đ°", + "add_tag": "Đ”Đ°Đ´Đ°Ņ†ŅŒ Ņ‚ŅĐŗ", "add_to": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ҃â€Ļ", "add_to_album": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ҃ аĐģŅŒĐąĐžĐŧ", "add_to_album_bottom_sheet_added": "ДададзĐĩĐŊа да {album}", @@ -33,28 +34,30 @@ "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": "ĐĄŅ‚Đ˛Đ°Ņ€Ņ‹Ņ†ŅŒ Ņ€ŅĐˇĐĩŅ€Đ˛ĐžĐ˛ŅƒŅŽ ĐēĐžĐŋŅ–ŅŽ ĐąĐ°ĐˇŅ‹ даĐŊҋ҅", "backup_database_enable_description": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ Ņ€ŅĐˇĐĩŅ€Đ˛Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐąĐ°ĐˇŅ‹ даĐŊҋ҅", "backup_keep_last_amount": "КоĐģҌĐēĐ°ŅŅ†ŅŒ ĐŋаĐŋŅŅ€ŅĐ´ĐŊŅ–Ņ… Ņ€ŅĐˇĐĩŅ€Đ˛ĐžĐ˛Ņ‹Ņ… ĐēĐžĐŋŅ–Đš Đ´ĐģŅ ĐˇĐ°Ņ…Đ°Đ˛Đ°ĐŊĐŊŅ", "backup_settings": "НаĐģĐ°Đ´Ņ‹ Ņ€ŅĐˇĐĩŅ€Đ˛ĐžĐ˛Đ°ĐŗĐ° ĐēаĐŋŅ–ŅĐ˛Đ°ĐŊĐŊŅ", - "backup_settings_description": "ĐšŅ–Ņ€Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐŊаĐģадаĐŧŅ– даĐŧĐŋа ĐąĐ°ĐˇŅ‹ дадСĐĩĐŊҋ҅. Đ—Đ°ŅžĐ˛Đ°ĐŗĐ°: ĐŗŅŅ‚Ņ‹Ņ ĐˇĐ°Đ´Đ°Ņ‡Ņ‹ ĐŊĐĩ ĐēаĐŊŅ‚Ņ€Đ°ĐģŅŽŅŽŅ†Ņ†Đ°, Ņ– Ņž Đ˛Ņ‹ĐŋадĐē҃ ĐŊŅŅžĐ´Đ°Ņ‡Ņ‹ ĐŋавĐĩдаĐŧĐģĐĩĐŊĐŊĐĩ адĐŋŅ€Đ°ŅžĐģĐĩĐŊа ĐŊĐĩ ĐąŅƒĐ´ĐˇĐĩ.", + "backup_settings_description": "ĐšŅ–Ņ€Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐŊаĐģадаĐŧŅ– Ņ€ŅĐˇĐĩŅ€Đ˛Đ°Đ˛Đ°ĐŊĐŊŅ ĐąĐ°ĐˇŅ‹ даĐŊҋ҅.", "cleared_jobs": "ĐŅ‡Ņ‹ŅˆŅ‡Đ°ĐŊŅ‹ СадаĐŊĐŊŅ– Đ´ĐģŅ: {job}", - "config_set_by_file": "КаĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ‹Ņ Ņž ĐˇĐ°Ņ€Đ°Đˇ ŅƒŅŅ‚Đ°ĐģŅĐ˛Đ°ĐŊа ĐŋŅ€Đ°Đˇ Ņ„Đ°ĐšĐģ ĐēаĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ‹Ņ–", - "confirm_delete_library": "Đ’Ņ‹ ŅžĐŋŅŅžĐŊĐĩĐŊŅ‹ ŅˆŅ‚Đž ĐļадаĐĩ҆Đĩ Đ˛Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ {library} ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐē҃?", + "config_set_by_file": "КаĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ‹Ņ ĐˇĐ°Ņ€Đ°Đˇ ŅƒŅŅ‚Đ°ĐģŅĐ˛Đ°ĐŊа ĐŋŅ€Đ°Đˇ Ņ„Đ°ĐšĐģ ĐēаĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ‹Ņ–", + "confirm_delete_library": "Đ’Ņ‹ ŅžĐŋŅŅžĐŊĐĩĐŊŅ‹ ŅˆŅ‚Đž ĐļадаĐĩ҆Đĩ Đ˛Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐē҃ {library}?", "confirm_delete_library_assets": "Đ’Ņ‹ ŅžĐŋŅŅžĐŊĐĩĐŊŅ‹, ŅˆŅ‚Đž Ņ…ĐžŅ‡Đ°Ņ†Đĩ Đ˛Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ĐŗŅŅ‚ŅƒŅŽ ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐē҃? Đ“ŅŅ‚Đ° ĐŋŅ€Ņ‹Đ˛ŅĐ´ĐˇĐĩ да Đ˛Ņ‹Đ´Đ°ĐģĐĩĐŊĐŊŅ {count, plural, one {# аĐēŅ‚Ņ‹Đ˛Ņƒ} other {ŅƒŅŅ–Ņ… # аĐēŅ‚Ņ‹Đ˛Đ°Ņž}}, ŅĐēŅ–Ņ СĐŧŅŅˆŅ‡Đ°ŅŽŅ†Ņ†Đ° Ņž Immich, Ņ– ĐŗŅŅ‚Đ° дСĐĩŅĐŊĐŊĐĩ ĐŊĐĩĐŧĐ°ĐŗŅ‡Ņ‹Đŧа ĐąŅƒĐ´ĐˇĐĩ адĐŧŅĐŊŅ–Ņ†ŅŒ. ФаКĐģŅ‹ ĐˇĐ°ŅŅ‚Đ°ĐŊŅƒŅ†Ņ†Đ° ĐŊа Đ´Ņ‹ŅĐē҃.", "confirm_email_below": "Каб ĐŋĐ°Ņ†Đ˛ĐĩŅ€Đ´ĐˇŅ–Ņ†ŅŒ, ŅƒĐ˛ŅĐ´ĐˇŅ–Ņ†Đĩ \"{email}\" ĐŊŅ–ĐļŅĐš", "confirm_reprocess_all_faces": "Đ’Ņ‹ ŅžĐŋŅŅžĐŊĐĩĐŊŅ‹, ŅˆŅ‚Đž Ņ…ĐžŅ‡Đ°Ņ†Đĩ ĐŋĐĩŅ€Đ°Đ°ĐŋŅ€Đ°Ņ†Đ°Đ˛Đ°Ņ†ŅŒ ҃ҁĐĩ Ņ‚Đ˛Đ°Ņ€Ņ‹? Đ“ŅŅ‚Đ° Ņ‚Đ°ĐēŅĐ°Đŧа ĐŋŅ€Ņ‹Đ˛ŅĐ´ĐˇĐĩ да Đ˛Ņ‹Đ´Đ°ĐģĐĩĐŊĐŊŅ Ņ–ĐŧŅ ĐģŅŽĐ´ĐˇĐĩĐš.", "confirm_user_password_reset": "Đ’Ņ‹ ŅžĐŋŅŅžĐŊĐĩĐŊŅ‹ Ņž ҂ҋĐŧ, ŅˆŅ‚Đž ĐļадаĐĩ҆Đĩ ҁĐēŅ–ĐŊŅƒŅ†ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ {user}?", + "confirm_user_pin_code_reset": "Đ’Ņ‹ ŅžĐŋŅŅžĐŊĐĩĐŊŅ‹ Ņž ҂ҋĐŧ, ŅˆŅ‚Đž ĐļадаĐĩ҆Đĩ ҁĐēŅ–ĐŊŅƒŅ†ŅŒ PIN-ĐēОд {user}?", "create_job": "ĐĄŅ‚Đ˛Đ°Ņ€Ņ‹Ņ†ŅŒ СадаĐŊĐŊĐĩ", "cron_expression": "Đ’Ņ‹Ņ€Đ°Đˇ Cron", "cron_expression_description": "ĐŖŅŅ‚Đ°ĐģŅŽĐšŅ†Đĩ Ņ–ĐŊŅ‚ŅŅ€Đ˛Đ°Đģ ҁĐēаĐŊаваĐŊĐŊŅ, Đ˛Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚ĐžŅžĐ˛Đ°ŅŽŅ‡Ņ‹ Ņ„Đ°Ņ€ĐŧĐ°Ņ‚ cron. ДĐģŅ Đ°Ņ‚Ņ€Ņ‹ĐŧаĐŊĐŊŅ Đ´Đ°Đ´Đ°Ņ‚ĐēОваК Ņ–ĐŊŅ„Đ°Ņ€ĐŧĐ°Ņ†Ņ‹Ņ–, ĐēаĐģŅ– ĐģĐ°ŅĐēа, ĐˇĐ˛ŅŅ€ĐŊҖ҆ĐĩŅŅ, ĐŊаĐŋҀҋĐēĐģад, да Crontab Guru", - "cron_expression_presets": "ĐŸŅ€Đ°Đ´ŅƒŅŅ‚Đ°ĐŊОвĐēŅ– Đ˛Ņ‹Ņ€Đ°ĐˇĐ°Ņž Cron", + "cron_expression_presets": "ĐŸŅ€Đ°Đ´ŅƒŅŅ‚Đ°ĐŊĐžŅžĐēŅ– Đ˛Ņ‹Ņ€Đ°ĐˇĐ°Ņž Cron", "disable_login": "АдĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ ŅƒĐ˛Đ°Ņ…ĐžĐ´", "duplicate_detection_job_description": "ЗаĐŋŅƒŅŅ†Ņ–Ņ†ŅŒ ĐŧĐ°ŅˆŅ‹ĐŊĐŊаĐĩ ĐŊĐ°Đ˛ŅƒŅ‡Đ°ĐŊĐŊĐĩ ĐŊа аĐēŅ‚Ņ‹Đ˛Đ°Ņ… Đ´ĐģŅ Đ˛Ņ‹ŅŅžĐģĐĩĐŊĐŊŅ ĐŋадОйĐŊҋ҅ Đ˛Ņ‹ŅŅž. ЗаĐģĐĩĐļŅ‹Ņ†ŅŒ ад Smart Search", "exclusion_pattern_description": "ШайĐģĐžĐŊŅ‹ Đ˛Ņ‹ĐēĐģŅŽŅ‡ŅĐŊĐŊŅ даСваĐģŅŅŽŅ†ŅŒ Ņ–ĐŗĐŊĐ°Ņ€Đ°Đ˛Đ°Ņ†ŅŒ Ņ„Đ°ĐšĐģŅ‹ Ņ– ĐŋаĐŋĐēŅ– ĐŋҀҋ ҁĐēаĐŊаваĐŊĐŊŅ– Đ˛Đ°ŅˆĐ°Đš ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēŅ–. Đ“ŅŅ‚Đ° ĐēĐ°Ņ€Ņ‹ŅĐŊа, ĐēаĐģŅ– Ņž Đ˛Đ°Ņ Ņ‘ŅŅ†ŅŒ ĐŋаĐŋĐēŅ–, ŅĐēŅ–Ņ СĐŧŅŅˆŅ‡Đ°ŅŽŅ†ŅŒ Ņ„Đ°ĐšĐģŅ‹, ŅĐēŅ–Ņ Đ˛Ņ‹ ĐŊĐĩ Ņ…ĐžŅ‡Đ°Ņ†Đĩ Ņ–ĐŧĐŋĐ°Ņ€Ņ‚Đ°Đ˛Đ°Ņ†ŅŒ, ĐŊаĐŋҀҋĐēĐģад, Ņ„Đ°ĐšĐģŅ‹ RAW.", @@ -71,15 +74,272 @@ "image_fullsize_enabled_description": "ĐĄŅ‚Đ˛Đ°Ņ€Đ°Ņ†ŅŒ Đ˛Ņ‹ŅĐ˛Ņƒ Ņž ĐŋĐžŅžĐŊŅ‹Đŧ ĐŋаĐŧĐĩҀҋ Đ´ĐģŅ Ņ„Đ°Ņ€ĐŧĐ°Ņ‚Đ°Ņž, ŅˆŅ‚Đž ĐŊĐĩ ĐŋŅ€Ņ‹Đ´Đ°Ņ‚ĐŊŅ‹Ņ Đ´ĐģŅ Đ˛ŅĐą. КаĐģŅ– ŅžĐēĐģŅŽŅ‡Đ°ĐŊа ĐžĐŋŅ†Ņ‹Ņ \"ĐĐ´Đ´Đ°Đ˛Đ°Ņ†ŅŒ ĐŋĐĩŅ€Đ°Đ˛Đ°ĐŗŅƒ ŅžĐąŅƒĐ´Đ°Đ˛Đ°ĐŊаК ĐŋŅ€Đ°ŅĐ˛Đĩ\", ĐŋŅ€Đ°ĐŗĐģŅĐ´Ņ‹ Đ˛Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚ĐžŅžĐ˛Đ°ŅŽŅ†Ņ†Đ° ĐŊĐĩĐŋĐ°ŅŅ€ŅĐ´ĐŊа ĐąĐĩС ĐēаĐŊвĐĩŅ€Ņ‚Đ°Ņ†Ņ‹Ņ–. НĐĩ ŅžĐŋĐģŅ‹Đ˛Đ°Đĩ ĐŊа Đ˛ŅĐą-ĐŋŅ€Ņ‹Đ´Đ°Ņ‚ĐŊŅ‹Ņ Ņ„Đ°Ņ€ĐŧĐ°Ņ‚Ņ‹, Ņ‚Đ°ĐēŅ–Ņ ŅĐē JPEG.", "image_fullsize_quality_description": "Đ¯ĐēĐ°ŅŅ†ŅŒ Đ˛Ņ‹ŅĐ˛Ņ‹ Ņž ĐŋĐžŅžĐŊŅ‹Đŧ ĐŋаĐŧĐĩҀҋ ад 1 да 100. БоĐģҌ҈ Đ˛Ņ‹ŅĐžĐēаĐĩ СĐŊĐ°Ņ‡ŅĐŊĐŊĐĩ ĐģĐĩĐŋŅˆĐ°Đĩ, аĐģĐĩ ĐŋŅ€Ņ‹Đ˛ĐžĐ´ĐˇŅ–Ņ†ŅŒ да ĐŋавĐĩĐģŅ–Ņ‡ŅĐŊĐŊŅ ĐŋаĐŧĐĩŅ€Ņƒ Ņ„Đ°ĐšĐģа.", "image_fullsize_title": "НаĐģĐ°Đ´Ņ‹ Đ˛Ņ‹ŅĐ˛Ņ‹ Ņž ĐŋĐžŅžĐŊŅ‹Đŧ ĐŋаĐŧĐĩҀҋ", + "image_prefer_embedded_preview_setting_description": "Đ’Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚ĐžŅžĐ˛Đ°Ņ†ŅŒ ŅƒĐąŅƒĐ´Đ°Đ˛Đ°ĐŊŅ‹Ņ ĐŋŅ€Đ°ŅĐ˛Ņ‹ Ņž RAW-Ņ„ĐžŅ‚Đ°ĐˇĐ´Ņ‹ĐŧĐēĐ°Ņ… Ņž ŅĐēĐ°ŅŅ†Ņ– ŅžĐ˛Đ°Ņ…ĐžĐ´ĐŊҋ҅ дадСĐĩĐŊҋ҅ Đ´ĐģŅ аĐŋŅ€Đ°Ņ†ĐžŅžĐēŅ– ĐŧаĐģŅŽĐŊĐēĐ°Ņž, ĐēаĐģŅ– ĐŧĐ°ĐŗŅ‡Ņ‹Đŧа. Đ“ŅŅ‚Đ° даСваĐģŅĐĩ Đ°Ņ‚Ņ€Ņ‹ĐŧĐ°Ņ†ŅŒ йОĐģҌ҈ даĐēĐģадĐŊŅ‹Ņ ĐēĐžĐģĐĩҀҋ Đ´ĐģŅ ĐŊĐĩĐēĐ°Ņ‚ĐžŅ€Ņ‹Ņ… Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ°Ņž, аĐģĐĩ Đļ ŅĐēĐ°ŅŅ†ŅŒ ĐŋŅ€Đ°ŅŅž СаĐģĐĩĐļŅ‹Ņ†ŅŒ ад ĐēаĐŧĐĩҀҋ, Ņ– ĐŊа Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐĩ ĐŧĐžĐļа ĐąŅ‹Ņ†ŅŒ йОĐģҌ҈ Đ°Ņ€Ņ‚ŅŅ„Đ°ĐēŅ‚Đ°Ņž ҁ҆ҖҁĐē҃.", + "image_prefer_wide_gamut": "ĐĐ´Đ´Đ°Ņ†ŅŒ ĐŋĐĩŅ€Đ°Đ˛Đ°ĐŗŅƒ ŅˆŅ‹Ņ€ĐžĐēаК ĐŗĐ°ĐŧĐĩ", "image_preview_title": "НаĐģĐ°Đ´Ņ‹ ĐŋаĐŋŅŅ€ŅĐ´ĐŊŅĐŗĐ° ĐŋŅ€Đ°ĐŗĐģŅĐ´Ņƒ", "image_quality": "Đ¯ĐēĐ°ŅŅ†ŅŒ", "image_resolution": "Đ Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŒ", "image_settings": "НаĐģĐ°Đ´Ņ‹ Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ°", - "image_settings_description": "ĐšŅ–Ņ€ŅƒĐšŅ†Đĩ ŅĐēĐ°ŅŅ†ŅŽ Ņ– Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŽ ŅĐŗĐĩĐŊĐĩŅ€Ņ‹Ņ€Đ°Đ˛Đ°ĐŊҋ҅ Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ°Ņž" + "image_settings_description": "ĐšŅ–Ņ€ŅƒĐšŅ†Đĩ ŅĐēĐ°ŅŅ†ŅŽ Ņ– Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŽ ŅĐŗĐĩĐŊĐĩŅ€Ņ‹Ņ€Đ°Đ˛Đ°ĐŊҋ҅ Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ°Ņž", + "library_created": "ĐĄŅ‚Đ˛ĐžŅ€Đ°ĐŊа ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēа: {library}", + "library_deleted": "Đ‘Ņ–ĐąĐģŅ–ŅŅ‚ŅĐēа Đ˛Ņ‹Đ´Đ°ĐģĐĩĐŊа", + "map_dark_style": "ĐĻŅ‘ĐŧĐŊŅ‹ ҁ҂ҋĐģҌ", + "map_enable_description": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ Ņ„ŅƒĐŊĐē҆ҋҖ ĐēĐ°Ņ€Ņ‚Ņ‹", + "map_gps_settings": "НаĐģĐ°Đ´Ņ‹ ĐēĐ°Ņ€Ņ‚Ņ‹ Ņ– GPS", + "map_light_style": "ХвĐĩŅ‚ĐģŅ‹ ҁ҂ҋĐģҌ", + "map_settings": "ĐšĐ°Ņ€Ņ‚Đ°", + "map_settings_description": "ĐšŅ–Ņ€Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐŊаĐģадаĐŧŅ– ĐēĐ°Ņ€Ņ‚Ņ‹", + "map_style_description": "URL-Đ°Đ´Ņ€Đ°Ņ style.json Ņ‚ŅĐŧŅ‹ ĐēĐ°Ņ€Ņ‚Ņ‹", + "metadata_settings": "НаĐģĐ°Đ´Ņ‹ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊҋ҅", + "oauth_button_text": "ĐĸŅĐēҁ҂ ĐēĐŊĐžĐŋĐēŅ–", + "oauth_settings": "OAuth", + "system_settings": "ĐĄŅ–ŅŅ‚ŅĐŧĐŊŅ‹Ņ ĐŊаĐģĐ°Đ´Ņ‹", + "theme_settings": "НаĐģĐ°Đ´Ņ‹ Ņ‚ŅĐŧŅ‹", + "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_audio_codec": "ĐŅƒĐ´Ņ‹ŅĐēĐžĐ´ŅĐē", + "transcoding_video_codec": "Đ’Ņ–Đ´ŅĐ°ĐēĐžĐ´ŅĐē", + "trash_settings": "НаĐģĐ°Đ´Ņ‹ ҁĐŧĐĩŅ‚ĐŊҖ҆ҋ", + "trash_settings_description": "ĐšŅ–Ņ€Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐŊаĐģадаĐŧŅ– ҁĐŧĐĩŅ‚ĐŊҖ҆ҋ", + "version_check_settings": "ĐŸŅ€Đ°Đ˛ĐĩŅ€Đēа вĐĩҀҁҖҖ", + "version_check_settings_description": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ/адĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ аĐŋĐ°Đ˛ŅŅˆŅ‡ŅĐŊĐŊŅ– ай ĐŊОваК вĐĩҀҁҖҖ" }, + "advanced_settings_troubleshooting_title": "Đ’Ņ‹ĐŋŅ€Đ°ŅžĐģĐĩĐŊĐŊĐĩ ĐŊĐĩĐŋаĐģадаĐē", + "album_added": "АĐģŅŒĐąĐžĐŧ дададСĐĩĐŊŅ‹", + "album_name": "Назва аĐģŅŒĐąĐžĐŧа", + "album_remove_user": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа?", + "album_updated": "АĐģŅŒĐąĐžĐŧ айĐŊĐžŅžĐģĐĩĐŊŅ‹", + "albums": "АĐģŅŒĐąĐžĐŧŅ‹", + "all": "ĐŖŅĐĩ", + "all_albums": "ĐŖŅĐĩ аĐģŅŒĐąĐžĐŧŅ‹", + "all_people": "ĐŖŅĐĩ ĐģŅŽĐ´ĐˇŅ–", + "all_videos": "ĐŖŅĐĩ Đ˛Ņ–Đ´ŅĐ°", + "app_bar_signout_dialog_ok": "ĐĸаĐē", + "app_bar_signout_dialog_title": "Đ’Ņ‹ĐšŅŅ†Ņ–", + "app_settings": "НаĐģĐ°Đ´Ņ‹ ĐŋŅ€Đ°ĐŗŅ€Đ°ĐŧŅ‹", + "archive": "ĐŅ€Ņ…Ņ–Ņž", + "archive_size": "ПаĐŧĐĩŅ€ Đ°Ņ€Ņ…Ņ–Đ˛Đ°", + "asset_uploading": "ЗаĐŋаĐŧĐŋĐžŅžĐ˛Đ°ĐŊĐŊĐĩâ€Ļ", + "back": "Назад", + "backup_all": "ĐŖŅĐĩ", + "backup_controller_page_background_wifi": "ĐĸĐžĐģҌĐēŅ– ĐŋŅ€Đ°Đˇ Wi-Fi", + "buy": "ĐšŅƒĐŋŅ–Ņ†ŅŒ Immich", + "cache_settings_clear_cache_button": "ĐŅ‡Ņ‹ŅŅ†Ņ–Ņ†ŅŒ ĐēŅŅˆ", + "cache_settings_tile_title": "ЛаĐēаĐģҌĐŊаĐĩ ŅŅ…ĐžĐ˛Ņ–ŅˆŅ‡Đ°", + "cancel": "ĐĄĐēĐ°ŅĐ°Đ˛Đ°Ņ†ŅŒ", + "cancel_search": "ĐĄĐēĐ°ŅĐ°Đ˛Đ°Ņ†ŅŒ ĐŋĐžŅˆŅƒĐē", + "canceled": "ĐĄĐēĐ°ŅĐ°Đ˛Đ°ĐŊа", + "city": "Đ“ĐžŅ€Đ°Đ´", + "clear": "ĐŅ‡Ņ‹ŅŅ†Ņ–Ņ†ŅŒ", + "clear_all": "ĐŅ‡Ņ‹ŅŅ†Ņ–Ņ†ŅŒ ŅƒŅŅ‘", + "client_cert_dialog_msg_confirm": "ОК", + "client_cert_enter_password": "ĐŖĐ˛ŅĐ´ĐˇŅ–Ņ†Đĩ ĐŋĐ°Ņ€ĐžĐģҌ", + "client_cert_import": "ІĐŧĐŋĐ°Ņ€Ņ‚", + "close": "ЗаĐēŅ€Ņ‹Ņ†ŅŒ", + "collapse": "Đ—ĐŗĐ°Ņ€ĐŊŅƒŅ†ŅŒ", + "collapse_all": "Đ—ĐŗĐ°Ņ€ĐŊŅƒŅ†ŅŒ ŅƒŅŅ‘", + "color": "КоĐģĐĩŅ€", + "color_theme": "КоĐģĐĩŅ€Đ°Đ˛Đ°Ņ Ņ‚ŅĐŧа", + "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": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ ĐŧĐĩŅŅ†Đ°ĐˇĐŊĐ°Ņ…ĐžĐ´ĐļаĐŊĐŊĐĩ", + "country": "ĐšŅ€Đ°Ņ–ĐŊа", + "cover": "ВоĐēĐģадĐēа", + "covers": "ВоĐēĐģадĐēŅ–", + "create": "ĐĄŅ‚Đ˛Đ°Ņ€Ņ‹Ņ†ŅŒ", + "create_album": "ĐĄŅ‚Đ˛Đ°Ņ€Ņ‹Ņ†ŅŒ аĐģŅŒĐąĐžĐŧ", + "create_album_page_untitled": "БĐĩС ĐŊĐ°ĐˇĐ˛Ņ‹", + "create_library": "ĐĄŅ‚Đ˛Đ°Ņ€Ņ‹Ņ†ŅŒ ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐē҃", + "create_link": "ĐĄŅ‚Đ˛Đ°Ņ€Ņ‹Ņ†ŅŒ ҁĐŋĐ°ŅŅ‹ĐģĐē҃", + "create_new_user": "ĐĄŅ‚Đ˛Đ°Ņ€Ņ‹Ņ†ŅŒ ĐŊĐžĐ˛Đ°ĐŗĐ° ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа", + "create_tag": "ĐĄŅ‚Đ˛Đ°Ņ€Ņ‹Ņ†ŅŒ Ņ‚ŅĐŗ", + "create_user": "ĐĄŅ‚Đ˛Đ°Ņ€Ņ‹Ņ†ŅŒ ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа", + "dark": "ĐĻŅ‘ĐŧĐŊĐ°Ņ", + "day": "ДзĐĩĐŊҌ", + "delete": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ", + "delete_album": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ аĐģŅŒĐąĐžĐŧ", + "delete_dialog_ok_force": "ĐŖŅŅ‘ адĐŊĐž Đ˛Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ", + "delete_dialog_title": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ĐŊĐ°ĐˇĐ°ŅžĐļĐ´Ņ‹", + "delete_face": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ Ņ‚Đ˛Đ°Ņ€", + "delete_key": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ĐēĐģŅŽŅ‡", + "delete_library": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐē҃", + "delete_link": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ҁĐŋĐ°ŅŅ‹ĐģĐē҃", + "delete_local_dialog_ok_force": "ĐŖŅŅ‘ адĐŊĐž Đ˛Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ", + "delete_others": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ Ņ–ĐŊŅˆŅ‹Ņ", + "delete_tag": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ Ņ‚ŅĐŗ", + "delete_user": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа", + "discord": "Discord", + "documentation": "ДаĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ‹Ņ", + "done": "Đ“Đ°Ņ‚ĐžĐ˛Đ°", + "download": "ĐĄĐŋаĐŧĐŋĐ°Đ˛Đ°Ņ†ŅŒ", + "download_canceled": "ĐĄĐŋаĐŧĐŋĐžŅžĐ˛Đ°ĐŊĐŊĐĩ ҁĐēĐ°ŅĐ°Đ˛Đ°ĐŊа", + "download_complete": "ĐĄĐŋаĐŧĐŋĐžŅžĐ˛Đ°ĐŊĐŊĐĩ СавĐĩŅ€ŅˆĐ°ĐŊа", + "download_enqueue": "ĐĄĐŋаĐŧĐŋĐžŅžĐ˛Đ°ĐŊĐŊĐĩ дададСĐĩĐŊа Ņž Ņ‡Đ°Ņ€ĐŗŅƒ", + "downloading": "ĐĄĐŋаĐŧĐŋĐžŅžĐ˛Đ°ĐŊĐŊĐĩ", + "edit": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ", + "edit_album": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ аĐģŅŒĐąĐžĐŧ", + "edit_avatar": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ Đ°Đ˛Đ°Ņ‚Đ°Ņ€", + "edit_date": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ Đ´Đ°Ņ‚Ņƒ", + "edit_date_and_time": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°ŅŒ Đ´Đ°Ņ‚Ņƒ Ņ– Ņ‡Đ°Ņ", + "edit_description": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ аĐŋŅ–ŅĐ°ĐŊĐŊĐĩ", + "edit_description_prompt": "Đ’Ņ‹ĐąĐĩҀҋ҆Đĩ ĐŊОваĐĩ аĐŋŅ–ŅĐ°ĐŊĐŊĐĩ:", + "edit_faces": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ Ņ‚Đ˛Đ°Ņ€Ņ‹", + "edit_import_path": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ ҈ĐģŅŅ… Ņ–ĐŧĐŋĐ°Ņ€Ņ‚Ņƒ", + "edit_import_paths": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ ҈ĐģŅŅ…Ņ– Ņ–ĐŧĐŋĐ°Ņ€Ņ‚Ņƒ", + "edit_key": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ ĐēĐģŅŽŅ‡", + "edit_link": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ ҁĐŋĐ°ŅŅ‹ĐģĐē҃", + "edit_location": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ ĐŧĐĩŅŅ†Đ°ĐˇĐŊĐ°Ņ…ĐžĐ´ĐļаĐŊĐŊĐĩ", + "edit_location_dialog_title": "МĐĩŅŅ†Đ°ĐˇĐŊĐ°Ņ…ĐžĐ´ĐļаĐŊĐŊĐĩ", + "edit_name": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ ĐŊĐ°ĐˇĐ˛Ņƒ", + "edit_people": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ ĐģŅŽĐ´ĐˇĐĩĐš", + "edit_tag": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ Ņ‚ŅĐŗ", + "edit_title": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ ĐˇĐ°ĐŗĐ°ĐģОваĐē", + "edit_user": "Đ ŅĐ´Đ°ĐŗĐ°Đ˛Đ°Ņ†ŅŒ ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа", + "edited": "ĐĐ´Ņ€ŅĐ´Đ°ĐŗĐ°Đ˛Đ°ĐŊа", + "editor": "Đ ŅĐ´Đ°ĐēŅ‚Đ°Ņ€", + "editor_close_without_save_prompt": "ЗĐŧĐĩĐŊŅ‹ ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ†ŅŒ ĐˇĐ°Ņ…Đ°Đ˛Đ°ĐŊŅ‹", + "editor_close_without_save_title": "ЗаĐēŅ€Ņ‹Ņ†ŅŒ Ņ€ŅĐ´Đ°ĐēŅ‚Đ°Ņ€?", + "editor_crop_tool_h2_aspect_ratios": "ĐĄŅƒĐ°Đ´ĐŊĐžŅŅ–ĐŊŅ‹ йаĐēĐžŅž", + "editor_crop_tool_h2_rotation": "ĐŸĐ°Đ˛Đ°Ņ€ĐžŅ‚", + "error": "ПаĐŧŅ‹ĐģĐēа", + "error_saving_image": "ПаĐŧŅ‹ĐģĐēа: {error}", + "exif": "Exif", + "exif_bottom_sheet_description": "Đ”Đ°Đ´Đ°Ņ†ŅŒ аĐŋŅ–ŅĐ°ĐŊĐŊĐĩ...", + "favorite": "ĐŖ Đ°ĐąŅ€Đ°ĐŊŅ‹Đŧ", + "favorite_or_unfavorite_photo": "Đ”Đ°Đ´Đ°Ņ†ŅŒ айО Đ˛Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ Ņ„ĐžŅ‚Đ° С Đ°ĐąŅ€Đ°ĐŊĐ°ĐŗĐ°", + "favorites": "ĐĐąŅ€Đ°ĐŊŅ‹Ņ", + "file_name": "Назва Ņ„Đ°ĐšĐģа", + "filename": "Назва Ņ„Đ°ĐšĐģа", + "filetype": "ĐĸŅ‹Đŋ Ņ„Đ°ĐšĐģа", + "filter": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€", + "forward": "НаĐŋĐĩŅ€Đ°Đ´", + "gcast_enabled": "Google Cast", + "general": "ĐĐŗŅƒĐģҌĐŊŅ‹Ņ", + "go_back": "Назад", + "go_to_folder": "ПĐĩŅ€Đ°ĐšŅŅ†Ņ– да ĐŋаĐŋĐēŅ–", + "hi_user": "Đ’Ņ–Ņ‚Đ°ĐĩĐŧ, {name} ({email})", + "hide_all_people": "ĐĄŅ…Đ°Đ˛Đ°Ņ†ŅŒ ŅƒŅŅ–Ņ… ĐģŅŽĐ´ĐˇĐĩĐš", + "hide_gallery": "ĐĄŅ…Đ°Đ˛Đ°Ņ†ŅŒ ĐŗĐ°ĐģĐĩŅ€ŅŅŽ", + "hide_named_person": "ĐĄŅ…Đ°Đ˛Đ°Ņ†ŅŒ {name}", + "hide_password": "ĐĄŅ…Đ°Đ˛Đ°Ņ†ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ", + "hide_person": "ĐĄŅ…Đ°Đ˛Đ°Ņ†ŅŒ Ņ‡Đ°ĐģавĐĩĐēа", + "image_viewer_page_state_provider_download_started": "ĐĄĐŋаĐŧĐŋĐžŅžĐ˛Đ°ĐŊĐŊĐĩ ĐŋĐ°Ņ‡Đ°ĐģĐžŅŅ", + "immich_logo": "Đ›Đ°ĐŗĐ°Ņ‚Ņ‹Đŋ Immich", + "interval": { + "day_at_onepm": "КоĐļĐŊŅ‹ дСĐĩĐŊҌ а 13-Đš ĐŗĐ°Đ´ĐˇŅ–ĐŊĐĩ", + "hours": "{hours, plural, one {КоĐļĐŊŅƒŅŽ ĐŗĐ°Đ´ĐˇŅ–ĐŊ҃} few {КоĐļĐŊŅ‹Ņ {hours, number} ĐŗĐ°Đ´ĐˇŅ–ĐŊŅ‹} many {КоĐļĐŊŅ‹Ņ {hours, number} ĐŗĐ°Đ´ĐˇŅ–ĐŊ} other {КоĐļĐŊŅ‹Ņ {hours, number} ĐŗĐ°Đ´ĐˇŅ–ĐŊ}}", + "night_at_midnight": "КоĐļĐŊŅƒŅŽ ĐŊĐžŅ‡ аĐŋĐžŅžĐŊĐ°Ņ‡Ņ‹", + "night_at_twoam": "КоĐļĐŊŅƒŅŽ ĐŊĐžŅ‡ а 2-Đš ĐŗĐ°Đ´ĐˇŅ–ĐŊĐĩ" + }, + "language": "Мова", + "library": "Đ‘Ņ–ĐąĐģŅ–ŅŅ‚ŅĐēа", + "light": "ХвĐĩŅ‚ĐģĐ°Ņ", + "login_form_back_button_text": "Назад", + "login_form_email_hint": "youremail@email.com", + "login_form_endpoint_hint": "http://your-server-ip:port", + "login_form_password_hint": "ĐŋĐ°Ņ€ĐžĐģҌ", + "login_form_save_login": "Đ—Đ°ŅŅ‚Đ°Đ˛Đ°Ņ†Ņ†Đ° Ņž ŅŅ–ŅŅ‚ŅĐŧĐĩ", + "main_menu": "ГаĐģĐžŅžĐŊаĐĩ ĐŧĐĩĐŊŅŽ", + "map_location_dialog_yes": "ĐĸаĐē", + "map_settings_dark_mode": "ĐĻŅ‘ĐŧĐŊŅ‹ Ņ€ŅĐļŅ‹Đŧ", + "map_settings_date_range_option_day": "АĐŋĐžŅˆĐŊŅ–Ņ 24 ĐŗĐ°Đ´ĐˇŅ–ĐŊŅ‹", + "map_settings_date_range_option_days": "АĐŋĐžŅˆĐŊŅ–Ņ… Đ´ĐˇŅ‘ĐŊ: {days}", + "map_settings_date_range_option_year": "АĐŋĐžŅˆĐŊŅ– ĐŗĐžĐ´", + "map_settings_date_range_option_years": "АĐŋĐžŅˆĐŊŅ–Ņ… ĐŗĐžĐ´: {years}", + "map_settings_dialog_title": "НаĐģĐ°Đ´Ņ‹ ĐēĐ°Ņ€Ņ‚Ņ‹", + "map_settings_theme_settings": "ĐĸŅĐŧа ĐēĐ°Ņ€Ņ‚Ņ‹", + "menu": "МĐĩĐŊŅŽ", + "minute": "ĐĨĐ˛Ņ–ĐģŅ–ĐŊа", + "month": "МĐĩŅŅŅ†", + "monthly_title_text_date_format": "MMMM y", + "my_albums": "МаĐĩ аĐģŅŒĐąĐžĐŧŅ‹", + "name": "ІĐŧŅ", + "name_or_nickname": "ІĐŧŅ айО ĐŋҁĐĩŅžĐ´Đ°ĐŊŅ–Đŧ", + "next": "ДаĐģĐĩĐš", + "no": "НĐĩ", + "offline": "Па-Са ҁĐĩŅ‚ĐēаК", + "ok": "ОК", + "online": "ĐŖ ҁĐĩ҂҆ҋ", + "open": "АдĐēŅ€Ņ‹Ņ†ŅŒ", + "or": "айО", + "partner_list_user_photos": "Đ¤ĐžŅ‚Đ° ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа {user}", + "pause": "ĐŸŅ€Ņ‹ĐŋŅ‹ĐŊŅ–Ņ†ŅŒ", + "people": "Đ›ŅŽĐ´ĐˇŅ–", + "permission_onboarding_back": "Назад", + "permission_onboarding_continue_anyway": "ĐŖŅŅ‘ адĐŊĐž ĐŋŅ€Đ°Ņ†ŅĐŗĐŊŅƒŅ†ŅŒ", + "photos": "Đ¤ĐžŅ‚Đ°", + "photos_and_videos": "Đ¤ĐžŅ‚Đ° Ņ– Đ˛Ņ–Đ´ŅĐ°", + "place": "МĐĩŅŅ†Đ°", + "places": "МĐĩҁ҆ҋ", + "port": "ĐŸĐžŅ€Ņ‚", + "previous": "ПаĐŋŅŅ€ŅĐ´ĐŊŅĐĩ", + "profile": "ĐŸŅ€ĐžŅ„Ņ–ĐģҌ", + "profile_drawer_app_logs": "Đ–ŅƒŅ€ĐŊаĐģŅ‹", + "profile_drawer_github": "GitHub", + "purchase_button_buy": "ĐšŅƒĐŋŅ–Ņ†ŅŒ", + "purchase_button_buy_immich": "ĐšŅƒĐŋŅ–Ņ†ŅŒ Immich", + "purchase_button_select": "Đ’Ņ‹ĐąŅ€Đ°Ņ†ŅŒ", + "remove": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ", + "remove_from_album": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ С аĐģŅŒĐąĐžĐŧа", + "remove_from_favorites": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ С Đ°ĐąŅ€Đ°ĐŊҋ҅", + "remove_tag": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ Ņ‚ŅĐŗ", + "remove_url": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ URL-Đ°Đ´Ņ€Đ°Ņ", + "remove_user": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа", + "rename": "ПĐĩŅ€Đ°ĐšĐŧĐĩĐŊĐ°Đ˛Đ°Ņ†ŅŒ", + "repository": "Đ ŅĐŋĐ°ĐˇŅ–Ņ‚ĐžŅ€Ņ‹Đš", + "reset": "ĐĄĐēŅ–ĐŊŅƒŅ†ŅŒ", + "reset_password": "ĐĄĐēŅ–ĐŊŅƒŅ†ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ", + "restore": "АдĐŊĐ°Đ˛Ņ–Ņ†ŅŒ", + "restore_all": "АдĐŊĐ°Đ˛Ņ–Ņ†ŅŒ ŅƒŅŅ‘", + "restore_user": "АдĐŊĐ°Đ˛Ņ–Ņ†ŅŒ ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа", + "resume": "ĐŖĐˇĐŊĐ°Đ˛Ņ–Ņ†ŅŒ", + "role": "Đ ĐžĐģŅ", + "role_editor": "Đ ŅĐ´Đ°ĐēŅ‚Đ°Ņ€", + "role_viewer": "ГĐģŅĐ´Đ°Ņ‡", + "save": "Đ—Đ°Ņ…Đ°Đ˛Đ°Ņ†ŅŒ", + "save_to_gallery": "Đ—Đ°Ņ…Đ°Đ˛Đ°Ņ†ŅŒ ҃ ĐŗĐ°ĐģĐĩŅ€ŅŅŽ", + "search_filter_date": "Đ”Đ°Ņ‚Đ°", + "search_filter_location": "МĐĩŅŅ†Đ°ĐˇĐŊĐ°Ņ…ĐžĐ´ĐļаĐŊĐŊĐĩ", + "search_filter_location_title": "Đ’Ņ‹ĐąĐĩҀҋ҆Đĩ ĐŧĐĩŅŅ†Đ°ĐˇĐŊĐ°Ņ…ĐžĐ´ĐļаĐŊĐŊĐĩ", + "search_filter_media_type": "ĐĸŅ‹Đŋ ĐŧĐĩĐ´Ņ‹Ņ", + "search_filter_media_type_title": "Đ’Ņ‹ĐąĐĩҀҋ҆Đĩ ҂ҋĐŋ ĐŧĐĩĐ´Ņ‹Ņ", + "search_page_screenshots": "Đ—Đ´Ņ‹ĐŧĐēŅ– ŅĐēŅ€Đ°ĐŊа", + "search_page_selfies": "ĐĄŅĐģ҄Җ", + "search_page_things": "Đ ŅŅ‡Ņ‹", + "search_page_your_map": "Đ’Đ°ŅˆĐ° ĐēĐ°Ņ€Ņ‚Đ°", + "second": "ĐĄĐĩĐē҃ĐŊда", + "send_message": "АдĐŋŅ€Đ°Đ˛Ņ–Ņ†ŅŒ ĐŋавĐĩдаĐŧĐģĐĩĐŊĐŊĐĩ", + "setting_languages_apply": "ĐŖĐļŅ‹Ņ†ŅŒ", + "setting_notifications_notify_never": "ĐŊŅ–ĐēĐžĐģŅ–", + "settings": "НаĐģĐ°Đ´Ņ‹", + "share_add_photos": "Đ”Đ°Đ´Đ°Ņ†ŅŒ Ņ„ĐžŅ‚Đ°", + "shared_album_section_people_title": "ЛЮДЗІ", + "shared_link_info_chip_metadata": "EXIF", + "sharing_page_empty_list": "ĐŸĐŖĐĄĐĸĐĢ ĐĄĐŸĐ†ĐĄ", + "sign_out": "Đ’Ņ‹ĐšŅŅ†Ņ–", + "sign_up": "Đ—Đ°Ņ€ŅĐŗŅ–ŅŅ‚Ņ€Đ°Đ˛Đ°Ņ†Ņ†Đ°", + "size": "ПаĐŧĐĩŅ€", + "sort_title": "Đ—Đ°ĐŗĐ°ĐģОваĐē", + "source": "ĐšŅ€Ņ‹ĐŊŅ–Ņ†Đ°", + "tag": "ĐĸŅĐŗ", + "tags": "ĐĸŅĐŗŅ–", + "theme": "ĐĸŅĐŧа", + "theme_selection": "Đ’Ņ‹ĐąĐ°Ņ€ Ņ‚ŅĐŧŅ‹", "timeline": "ĐĨŅ€ĐžĐŊŅ–Đēа", "total": "ĐŖŅŅĐŗĐž", + "trash": "ĐĄĐŧĐĩŅ‚ĐŊŅ–Ņ†Đ°", + "trash_page_delete_all": "Đ’Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ҃ҁĐĩ", + "trash_page_restore_all": "АдĐŊĐ°Đ˛Ņ–Ņ†ŅŒ ҃ҁĐĩ", + "trash_page_title": "ĐĄĐŧĐĩŅ‚ĐŊŅ–Ņ†Đ° ({count})", + "type": "ĐĸŅ‹Đŋ", + "undo": "ĐĐ´Ņ€Đ°ĐąŅ–Ņ†ŅŒ", + "upload": "ЗаĐŋаĐŧĐŋĐ°Đ˛Đ°Ņ†ŅŒ", + "upload_status_errors": "ПаĐŧŅ‹ĐģĐēŅ–", + "uploading": "ЗаĐŋаĐŧĐŋĐžŅžĐ˛Đ°ĐŊĐŊĐĩ", + "url": "URL-Đ°Đ´Ņ€Đ°Ņ", "user": "ĐšĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đē", + "user_has_been_deleted": "Đ“ŅŅ‚Ņ‹ ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đē ĐąŅ‹Ņž Đ˛Ņ‹Đ´Đ°ĐģĐĩĐŊŅ‹.", "user_id": "ID ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа", "user_purchase_settings": "ĐšŅƒĐŋĐģŅ", "user_purchase_settings_description": "ĐšŅ–Ņ€ŅƒĐšŅ†Đĩ ĐŋаĐē҃ĐŋĐēаĐŧŅ–", @@ -112,14 +372,14 @@ "view_next_asset": "ПаĐēĐ°ĐˇĐ°Ņ†ŅŒ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊŅ‹ ай'ĐĩĐēŅ‚", "view_previous_asset": "ĐŸŅ€Đ°ĐŗĐģŅĐ´ĐˇĐĩŅ†ŅŒ ĐŋаĐŋŅŅ€ŅĐ´ĐŊŅ– ай'ĐĩĐēŅ‚", "view_stack": "ĐŸŅ€Đ°ĐŗĐģŅĐ´ ŅŅ‚ŅĐēа", - "visibility_changed": "Đ’Ņ–Đ´ĐˇŅ–ĐŧĐ°ŅŅ†ŅŒ СĐŧŅĐŊŅ–ĐģĐ°ŅŅ Đ´ĐģŅ {count, plural, one {# Ņ‡Đ°ĐģавĐĩĐē(-Đ°Ņž)} Đ°ŅŅ‚Đ°Ņ‚ĐŊŅ–Ņ… {# Ņ‡Đ°ĐģавĐĩĐē}}", + "visibility_changed": "Đ‘Đ°Ņ‡ĐŊĐ°ŅŅ†ŅŒ СĐŧŅĐŊŅ–ĐģĐ°ŅŅ Đ´ĐģŅ {count, plural, one {# Ņ‡Đ°ĐģавĐĩĐēа} other {# Ņ‡Đ°ĐģавĐĩĐē}}", "waiting": "ЧаĐēĐ°ŅŽŅ†ŅŒ", "warning": "ПаĐŋŅŅ€ŅĐ´ĐļаĐŊĐŊĐĩ", "week": "ĐĸŅ‹Đ´ĐˇĐĩĐŊҌ", "welcome": "Đ’Ņ–Ņ‚Đ°ĐĩĐŧ", "welcome_to_immich": "Đ’Ņ–Ņ‚Đ°ĐĩĐŧ ҃ Immich", "year": "Год", - "years_ago": "{years, plural, one {# ĐŗĐžĐ´} other {# ĐŗĐ°Đ´ĐžŅž}} Ņ‚Đ°Đŧ҃", + "years_ago": "{years, plural, one {# ĐŗĐžĐ´} few {# ĐŗĐ°Đ´Ņ‹} many {# ĐŗĐ°Đ´ĐžŅž} other {# ĐŗĐ°Đ´ĐžŅž}} Ņ‚Đ°Đŧ҃", "yes": "ĐĸаĐē", "you_dont_have_any_shared_links": "ĐŖ Đ˛Đ°Ņ ĐŊŅĐŧа Đ°ĐąĐ°ĐŗŅƒĐģĐĩĐŊҋ҅ ҁĐŋĐ°ŅŅ‹ĐģаĐē", "zoom_image": "ĐŸĐ°Đ˛ŅĐģŅ–Ņ‡Ņ‹Ņ†ŅŒ Đ˛Ņ–Đ´Đ°Ņ€Ņ‹Ņ" diff --git a/i18n/bg.json b/i18n/bg.json index 2df3dd927d..327fd7c7df 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -166,6 +166,20 @@ "metadata_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊа ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēĐ¸Ņ‚Đĩ Са ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐŊи", "migration_job": "ĐœĐ¸ĐŗŅ€Đ°Ņ†Đ¸Ņ", "migration_job_description": "ĐœĐ¸ĐŗŅ€Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ĐŧиĐŊĐ¸Đ°Ņ‚ŅŽŅ€Đ¸Ņ‚Đĩ Са ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ и ĐģĐ¸Ņ†Đ° ĐēҊĐŧ ĐŊаК-ĐŊĐžĐ˛Đ°Ņ‚Đ° ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€Đ° ĐŊа ĐŋаĐŋĐēĐ¸Ņ‚Đĩ", + "nightly_tasks_cluster_faces_setting_description": "ИСĐŋҊĐģĐŊи Ņ€Đ°ĐˇĐŋОСĐŊаваĐŊĐĩ ĐŊа ĐģĐ¸Ņ†Đĩ Са ĐžŅ‚ĐēŅ€Đ¸Ņ‚Đ¸ ĐŊОви ĐģĐ¸Ņ†Đ°", + "nightly_tasks_cluster_new_faces_setting": "РаСĐŋОСĐŊаваĐŊĐĩ ĐŊа ĐŊОви ĐģĐ¸Ņ†Đ°", + "nightly_tasks_database_cleanup_setting": "Đ—Đ°Đ´Đ°Ņ‡Đ¸ ĐŋĐž ĐŋĐžŅ‡Đ¸ŅŅ‚Đ˛Đ°ĐŊĐĩ ĐŊа ĐąĐ°ĐˇĐ°Ņ‚Đ° даĐŊĐŊи", + "nightly_tasks_database_cleanup_setting_description": "ĐŸŅ€ĐĩĐŧĐ°Ņ…ĐŊи ŅŅ‚Đ°Ņ€Đ¸, ĐŊĐĩĐŊ҃ĐļĐŊи СаĐŋĐ¸ŅĐ¸ ĐžŅ‚ ĐąĐ°ĐˇĐ°Ņ‚Đ° даĐŊĐŊи", + "nightly_tasks_generate_memories_setting": "ĐĄŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ ĐŊа ҁĐŋĐžĐŧĐĩĐŊи", + "nightly_tasks_generate_memories_setting_description": "ĐĄŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ ĐŊа ĐŊОви ҁĐŋĐžĐŧĐĩĐŊи ĐžŅ‚ ŅŅŠŅ‰ĐĩŅŅ‚Đ˛ŅƒĐ˛Đ°Ņ‰Đ¸ ОйĐĩĐēŅ‚Đ¸", + "nightly_tasks_missing_thumbnails_setting": "ГĐĩĐŊĐĩŅ€Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ĐģиĐŋŅĐ˛Đ°Ņ‰Đ¸ ĐŧиĐŊĐ¸Đ°Ņ‚ŅŽŅ€Đ¸", + "nightly_tasks_missing_thumbnails_setting_description": "Đ”ĐžĐąĐ°Đ˛ŅĐŊĐĩ ĐŊа ОйĐĩĐēŅ‚Đ¸ ĐąĐĩС ĐŧиĐŊĐ¸Đ°Ņ‚ŅŽŅ€Đ° в ĐžĐŋĐ°ŅˆĐēĐ°Ņ‚Đ° Са ŅŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ ĐŊа ĐŧиĐŊĐ¸Đ°Ņ‚ŅŽŅ€Đ°", + "nightly_tasks_settings": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēа ĐŊа ĐˇĐ°Đ´Đ°Ņ‡Đ¸ Са ĐŋŅ€ĐĩС ĐŊĐžŅ‰Ņ‚Đ°", + "nightly_tasks_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊа ĐˇĐ°Đ´Đ°Ņ‡Đ¸Ņ‚Đĩ, иСĐŋҊĐģĐŊŅĐ˛Đ°ĐŊи ĐŋŅ€ĐĩС ĐŊĐžŅ‰Ņ‚Đ°", + "nightly_tasks_start_time_setting": "Đ’Ņ€ĐĩĐŧĐĩ Са ĐŊĐ°Ņ‡Đ°ĐģĐž", + "nightly_tasks_start_time_setting_description": "Đ’Ņ€ĐĩĐŧĐĩ, ĐēĐžĐŗĐ°Ņ‚Đž ŅŅŠŅ€Đ˛ŅŠŅ€Đ° ҉Đĩ СаĐŋĐžŅ‡ĐŊĐĩ иСĐŋҊĐģĐŊĐĩĐŊиĐĩ ĐŊа ĐŊĐžŅ‰ĐŊи ĐˇĐ°Đ´Đ°Ņ‡Đ¸", + "nightly_tasks_sync_quota_usage_setting": "ĐšĐ˛ĐžŅ‚Đ° Са ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Ņ", + "nightly_tasks_sync_quota_usage_setting_description": "ОбĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа ĐēĐ˛ĐžŅ‚Đ°Ņ‚Đ° ҁĐŋĐžŅ€ĐĩĐ´ Ņ‚ĐĩĐēŅƒŅ‰ĐžŅ‚Đž ĐŋĐžŅ‚Ņ€ĐĩĐąĐģĐĩĐŊиĐĩ", "no_paths_added": "ĐŅĐŧа дОйавĐĩĐŊи ĐŋŅŠŅ‚Đ¸Ņ‰Đ°", "no_pattern_added": "ĐŅĐŧа дОйавĐĩĐŊ ĐŧОдĐĩĐģ", "note_apply_storage_label_previous_assets": "ЗабĐĩĐģĐĩĐļĐēа: За да ĐŋŅ€Đ¸ĐģĐžĐļĐ¸Ņ‚Đĩ ĐĩŅ‚Đ¸ĐēĐĩŅ‚Đ° Са ŅŅŠŅ…Ņ€Đ°ĐŊĐĩĐŊиĐĩ ĐēҊĐŧ ĐŋŅ€ĐĩĐ´Đ˛Đ°Ņ€Đ¸Ņ‚ĐĩĐģĐŊĐž ĐēĐ°Ņ‡ĐĩĐŊи Ņ„Đ°ĐšĐģОвĐĩ, ŅŅ‚Đ°Ņ€Ņ‚Đ¸Ņ€Đ°ĐšŅ‚Đĩ", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "URI Са ĐŧОйиĐģĐŊĐž ĐŋŅ€ĐĩĐŊĐ°ŅĐžŅ‡Đ˛Đ°ĐŊĐĩ", "oauth_mobile_redirect_uri_override": "URI ĐŋŅ€ĐĩĐŊĐ°ŅĐžŅ‡Đ˛Đ°ĐŊĐĩ Са ĐŧОйиĐģĐŊи ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ°", "oauth_mobile_redirect_uri_override_description": "Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸ ĐēĐžĐŗĐ°Ņ‚Đž Đ´ĐžŅŅ‚Đ°Đ˛Ņ‡Đ¸Đēа Са OAuth ŅƒĐ´ĐžŅŅ‚ĐžĐ˛ĐĩŅ€ŅĐ˛Đ°ĐŊĐĩ ĐŊĐĩ ĐŋОСвОĐģŅĐ˛Đ° Са ĐŧОйиĐģĐŊи URI идĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚ĐžŅ€Đ¸, ĐēĐ°Ņ‚Đž ''{callback}''", + "oauth_role_claim": "ĐŸĐžŅ‚Đ˛ŅŠŅ€ĐļĐ´ĐĩĐŊиĐĩ ĐŊа Ņ€ĐžĐģŅ", + "oauth_role_claim_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ĐŋŅ€ĐĩĐ´ĐžŅŅ‚Đ°Đ˛ŅĐŊĐĩ ĐŊа адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚Đ¸Đ˛ĐŊи ĐŋŅ€Đ°Đ˛Đ° ĐŋŅ€Đ¸ ĐŊаĐģĐ¸Ņ‡Đ¸Đĩ ĐŊа Ņ‚ĐžĐ˛Đ° ĐŋĐžŅ‚Đ˛ŅŠŅ€ĐļĐĩĐŊиĐĩ. ĐŸĐžŅ‚Đ˛ŅŠŅ€ĐļĐ´ĐĩĐŊиĐĩŅ‚Đž ĐŧĐžĐļĐĩ да иĐŧа ŅŅ‚ĐžĐšĐŊĐžŅŅ‚ 'user' иĐģи 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊа ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēĐ¸Ņ‚Đĩ Са Đ˛Ņ…ĐžĐ´ ҁ OAuth", "oauth_settings_more_details": "За ĐŋОвĐĩ҇Đĩ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Са Ņ„ŅƒĐŊĐēŅ†Đ¸ĐžĐŊаĐģĐŊĐžŅŅ‚Ņ‚Đ°, ҁĐĩ ĐŋĐžŅ‚ŅŠŅ€ŅĐĩŅ‚Đĩ в docs.", @@ -357,6 +373,8 @@ "admin_password": "АдĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€ŅĐēа ĐŋĐ°Ņ€ĐžĐģа", "administration": "АдĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ†Đ¸Ņ", "advanced": "Đ Đ°ĐˇŅˆĐ¸Ņ€ĐĩĐŊĐž", + "advanced_settings_beta_timeline_subtitle": "ОĐŋĐ¸Ņ‚Đ°ĐšŅ‚Đĩ ĐŊĐžĐ˛Đ¸Ņ‚Đĩ Ņ„ŅƒĐŊĐēŅ†Đ¸Đ¸ ĐŊа ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩŅ‚Đž", + "advanced_settings_beta_timeline_title": "БĐĩŅ‚Đ° вĐĩŅ€ŅĐ¸Ņ ĐŊа Đ˛Ņ€ĐĩĐŧĐĩĐ˛Đ°Ņ‚Đ° ĐģиĐŊĐ¸Ņ", "advanced_settings_enable_alternate_media_filter_subtitle": "ĐŸŅ€Đ¸ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Ņ, иСĐŋĐžĐģĐˇĐ˛Đ°ĐšŅ‚Đĩ Ņ‚Đ°ĐˇĐ¸ ĐžĐŋŅ†Đ¸Ņ ĐēĐ°Ņ‚Đž Ņ„Đ¸ĐģŅ‚ŅŠŅ€, ĐžŅĐŊОваĐŊ ĐŊа ĐŋŅ€ĐžĐŧŅĐŊа ĐŊа дадĐĩĐŊ ĐēŅ€Đ¸Ņ‚ĐĩŅ€Đ¸Đ¸. ОĐŋĐ¸Ņ‚Đ°ĐšŅ‚Đĩ ŅĐ°ĐŧĐž в ҁĐģŅƒŅ‡Đ°Đš, ҇Đĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩŅ‚Đž иĐŧа ĐŋŅ€ĐžĐąĐģĐĩĐŧ ҁ ĐžŅ‚ĐēŅ€Đ¸Đ˛Đ°ĐŊĐĩ ĐŊа Đ˛ŅĐ¸Ņ‡Đēи аĐģĐąŅƒĐŧи.", "advanced_settings_enable_alternate_media_filter_title": "[ЕКСПЕРИМЕНĐĸАЛНО] ИСĐŋĐžĐģСваК Ņ„Đ¸ĐģŅ‚ŅŠŅ€Đ° ĐŊа аĐģŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊĐžŅ‚Đž ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž Са ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Ņ ĐŊа аĐģĐąŅƒĐŧи", "advanced_settings_log_level_title": "Ниво ĐŊа СаĐŋĐ¸Ņ в Đ´ĐŊĐĩвĐŊиĐēа: {level}", @@ -427,6 +445,7 @@ "app_settings": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи Đŧа ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩŅ‚Đž", "appears_in": "ИСĐģиСа в", "archive": "ĐŅ€Ņ…Đ¸Đ˛", + "archive_action_prompt": "{count} ŅĐ° дОйавĐĩĐŊи в ĐŅ€Ņ…Đ¸Đ˛Đ°", "archive_or_unarchive_photo": "ĐŅ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°ĐŊĐĩ иĐģи Đ´ĐĩĐ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ҁĐŊиĐŧĐēа", "archive_page_no_archived_assets": "НĐĩ ŅĐ° ĐŊаĐŧĐĩŅ€ĐĩĐŊи ОйĐĩĐēŅ‚Đ¸ в Đ°Ņ€Ņ…Đ¸Đ˛Đ°", "archive_page_title": "ĐŅ€Ņ…Đ¸Đ˛ ({count})", @@ -464,7 +483,6 @@ "assets": "ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", "assets_added_count": "ДобавĐĩĐŊĐž {count, plural, one {# asset} other {# assets}}", "assets_added_to_album_count": "ДобавĐĩĐŊ(и) ŅĐ° {count, plural, one {# аĐēŅ‚Đ¸Đ˛} other {# аĐēŅ‚Đ¸Đ˛Đ°}} в аĐģĐąŅƒĐŧа", - "assets_added_to_name_count": "ДобавĐĩĐŊ(и) ŅĐ° {count, plural, one {# аĐēŅ‚Đ¸Đ˛} other {# аĐēŅ‚Đ¸Đ˛Đ°}} ĐēҊĐŧ {hasName, select, true {{name}} other {ĐŊОв аĐģĐąŅƒĐŧ}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {ОбĐĩĐēŅ‚Đ° ĐŊĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ дОйави} other {ОбĐĩĐēŅ‚Đ¸Ņ‚Đĩ ĐŊĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ Đ´ĐžĐąĐ°Đ˛ŅŅ‚}} в аĐģĐąŅƒĐŧа", "assets_count": "{count, plural, one {# аĐēŅ‚Đ¸Đ˛} other {# аĐēŅ‚Đ¸Đ˛Đ°}}", "assets_deleted_permanently": "{count} ОйĐĩĐēŅ‚Đ° ŅĐ° Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚Đ¸ СавиĐŊĐ°ĐŗĐ¸", @@ -703,7 +721,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM yyyy", "dark": "ĐĸҊĐŧĐĩĐŊ", - "darkTheme": "ĐŸŅ€ĐĩвĐēĐģŅŽŅ‡Đ¸ ĐŊа Ņ‚ŅŠĐŧĐŊа Ņ‚ĐĩĐŧа", + "dark_theme": "ĐĸҊĐŧĐŊа Ņ‚ĐĩĐŧа", "date_after": "Đ”Đ°Ņ‚Đ° ҁĐģĐĩĐ´", "date_and_time": "Đ”Đ°Ņ‚Đ° и Ņ‡Đ°Ņ", "date_before": "Đ”Đ°Ņ‚Đ° ĐŋŅ€Đĩди", @@ -719,6 +737,7 @@ "default_locale": "ЛоĐēаĐģĐ¸ĐˇĐ°Ņ†Đ¸Ņ ĐŋĐž ĐŋĐžĐ´Ņ€Đ°ĐˇĐąĐ¸Ņ€Đ°ĐŊĐĩ", "default_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа Đ´Đ°Ņ‚Đ¸ и Ņ‡Đ¸ŅĐģа в ĐˇĐ°Đ˛Đ¸ŅĐ¸ĐŧĐžŅŅ‚ ĐžŅ‚ ĐĩСиĐēĐžĐ˛Đ°Ņ‚Đ° ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēа ĐŊа ĐąŅ€Đ°ŅƒĐˇŅŠŅ€Đ°", "delete": "Đ˜ĐˇŅ‚Ņ€Đ¸Đš", + "delete_action_prompt": "{count} ŅĐ° Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚Đ¸ СавиĐŊĐ°ĐŗĐ¸", "delete_album": "Đ˜ĐˇŅ‚Ņ€Đ¸Đš аĐģĐąŅƒĐŧ", "delete_api_key_prompt": "ĐĄĐ¸ĐŗŅƒŅ€ĐŊи Đģи ҁ҂Đĩ, ҇Đĩ Đ¸ŅĐēĐ°Ņ‚Đĩ да Đ¸ĐˇŅ‚Ņ€Đ¸ĐĩŅ‚Đĩ Ņ‚ĐžĐˇĐ¸ API ĐēĐģŅŽŅ‡?", "delete_dialog_alert": "ĐĸĐĩСи ОйĐĩĐēŅ‚Đ¸ ҉Đĩ ĐąŅŠĐ´Đ°Ņ‚ Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚Đ¸ СавиĐŊĐ°ĐŗĐ¸ и ĐžŅ‚ Immich ŅŅŠŅ€Đ˛ŅŠŅ€Đ° и ĐžŅ‚ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛ĐžŅ‚Đž", @@ -732,19 +751,20 @@ "delete_key": "Đ˜ĐˇŅ‚Ņ€Đ¸Đš ĐēĐģŅŽŅ‡", "delete_library": "Đ˜ĐˇŅ‚Ņ€Đ¸Đš йийĐģĐ¸ĐžŅ‚ĐĩĐēа", "delete_link": "Đ˜ĐˇŅ‚Ņ€Đ¸Đš ĐģиĐŊĐē", + "delete_local_action_prompt": "{count} ŅĐ° Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚Đ¸ ĐģĐžĐēаĐģĐŊĐž", "delete_local_dialog_ok_backed_up_only": "Đ˜ĐˇŅ‚Ņ€Đ¸Đš ĐģĐžĐēаĐģĐŊĐž ŅĐ°ĐŧĐž Đ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°ĐŊĐ¸Ņ‚Đĩ", "delete_local_dialog_ok_force": "Đ’ŅŠĐŋŅ€ĐĩĐēи Ņ‚ĐžĐ˛Đ° Đ¸ĐˇŅ‚Ņ€Đ¸Đš", "delete_others": "Đ˜ĐˇŅ‚Ņ€Đ¸Đš ĐžŅŅ‚Đ°ĐŊаĐģĐ¸Ņ‚Đĩ", "delete_shared_link": "Đ˜ĐˇŅ‚Ņ€Đ¸Đ˛Đ°ĐŊĐĩ ĐŊа ҁĐŋОдĐĩĐģĐĩĐŊ ĐģиĐŊĐē", "delete_shared_link_dialog_title": "Đ˜ĐˇŅ‚Ņ€Đ¸Đš ҁĐŋОдĐĩĐģĐĩĐŊĐ°Ņ‚Đ° Đ˛Ņ€ŅŠĐˇĐēа", "delete_tag": "Đ˜ĐˇŅ‚Ņ€Đ¸Đš Ņ‚Đ°Đŗ", - "delete_tag_confirmation_prompt": "ĐĄĐ¸ĐŗŅƒŅ€ĐŊи Đģи ҁ҂Đĩ, ҇Đĩ Đ¸ŅĐēĐ°Ņ‚Đĩ да Đ¸ĐˇŅ‚Ņ€Đ¸ĐĩŅ‚Đĩ Ņ‚Đ°Đŗ {tagName}?", + "delete_tag_confirmation_prompt": "ĐĄĐ¸ĐŗŅƒŅ€ĐŊи Đģи ҁ҂Đĩ, ҇Đĩ Đ¸ŅĐēĐ°Ņ‚Đĩ да Đ¸ĐˇŅ‚Ņ€Đ¸ĐĩŅ‚Đĩ Ņ‚Đ°ĐŗĐ° {tagName}?", "delete_user": "Đ˜ĐˇŅ‚Ņ€Đ¸Đš ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ", "deleted_shared_link": "Đ˜ĐˇŅ‚Ņ€Đ¸Ņ‚ ҁĐŋОдĐĩĐģĐĩĐŊ ĐģиĐŊĐē", "deletes_missing_assets": "Đ˜ĐˇŅ‚Ņ€Đ¸Đ˛Đ° Ņ„Đ°ĐšĐģОвĐĩ, ĐēĐžĐ¸Ņ‚Đž ĐģиĐŋŅĐ˛Đ°Ņ‚ ĐŊа Đ´Đ¸ŅĐēа", "description": "ОĐŋĐ¸ŅĐ°ĐŊиĐĩ", "description_input_hint_text": "Добави ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ...", - "description_input_submit_error": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ОйĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩŅ‚Đž. За ĐŋĐžĐ´Ņ€ĐžĐąĐŊĐžŅŅ‚Đ¸ виĐļ в Đ´ĐŊĐĩвĐŊиĐēа", + "description_input_submit_error": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ОйĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩŅ‚Đž. За ĐŋĐžĐ´Ņ€ĐžĐąĐŊĐžŅŅ‚Đ¸ виĐļŅ‚Đĩ в Đ´ĐŊĐĩвĐŊиĐēа", "details": "ДĐĩŅ‚Đ°ĐšĐģи", "direction": "ĐŸĐžŅĐžĐēа", "disabled": "ИСĐēĐģŅŽŅ‡ĐĩĐŊĐž", @@ -762,6 +782,7 @@ "documentation": "ДоĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Đ¸Ņ", "done": "Đ“ĐžŅ‚ĐžĐ˛Đž", "download": "Đ˜ĐˇŅ‚ĐĩĐŗĐģи", + "download_action_prompt": "Đ—Đ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа {count} ОйĐĩĐēŅ‚Đ°", "download_canceled": "Đ˜ĐˇŅ‚ĐĩĐŗĐģŅĐŊĐĩŅ‚Đž Đĩ ĐžŅ‚ĐŧĐĩĐŊĐĩĐŊĐž", "download_complete": "Đ˜ĐˇŅ‚ĐĩĐŗĐģŅĐŊĐĩŅ‚Đž ĐˇĐ°Đ˛ŅŠŅ€ŅˆĐ¸", "download_enqueue": "Đ˜ĐˇŅ‚ĐĩĐŗĐģŅĐŊĐĩŅ‚Đž Đĩ дОйавĐĩĐŊĐž в ĐžĐŋĐ°ŅˆĐēĐ°Ņ‚Đ°", @@ -799,6 +820,7 @@ "edit_key": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ĐēĐģŅŽŅ‡", "edit_link": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ĐģиĐŊĐē", "edit_location": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩŅ‚Đž", + "edit_location_action_prompt": "{count} ĐģĐžĐēĐ°Ņ†Đ¸Đ¸ ŅĐ° Ņ€ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊи", "edit_location_dialog_title": "МĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", "edit_name": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа иĐŧĐĩ", "edit_people": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа Ņ…ĐžŅ€Đ°", @@ -984,6 +1006,7 @@ "failed_to_load_assets": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐˇĐ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", "failed_to_load_folder": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐˇĐ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа ĐŋаĐŋĐēа", "favorite": "Đ›ŅŽĐąĐ¸Đŧ", + "favorite_action_prompt": "{count} ŅĐ° дОйавĐĩĐŊи в Đ›ŅŽĐąĐ¸Đŧи", "favorite_or_unfavorite_photo": "Добави иĐģи ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊи ҁĐŊиĐŧĐēа ĐžŅ‚ Đ›ŅŽĐąĐ¸Đŧи", "favorites": "Đ›ŅŽĐąĐ¸Đŧи", "favorites_page_no_favorites": "НĐĩ ŅĐ° ĐŊаĐŧĐĩŅ€ĐĩĐŊи ĐģŅŽĐąĐ¸Đŧи ОйĐĩĐēŅ‚Đ¸", @@ -1127,6 +1150,7 @@ "library_page_sort_created": "Đ”Đ°Ņ‚Đ° ĐŊа ŅŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ", "library_page_sort_last_modified": "ĐŸĐžŅĐģĐĩĐ´ĐŊа ĐŋŅ€ĐžĐŧŅĐŊа", "library_page_sort_title": "Đ—Đ°ĐŗĐģавиĐĩ ĐŊа аĐģĐąŅƒĐŧа", + "licenses": "Đ›Đ¸Ņ†ĐĩĐŊСи", "light": "ХвĐĩŅ‚ĐģĐž", "like_deleted": "ĐšĐ°Ņ‚Đž Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚", "link_motion_video": "ЛиĐŊĐē ĐēҊĐŧ видĐĩĐž", @@ -1246,6 +1270,7 @@ "more": "ĐžŅ‰Đĩ", "move": "ĐŸŅ€ĐĩĐŧĐĩŅŅ‚Đ¸", "move_off_locked_folder": "ИСвади ĐžŅ‚ СаĐēĐģŅŽŅ‡ĐĩĐŊĐ°Ņ‚Đ° ĐŋаĐŋĐēа", + "move_to_lock_folder_action_prompt": "{count} ŅĐ° дОйавĐĩĐŊи в СаĐēĐģŅŽŅ‡ĐĩĐŊĐ°Ņ‚Đ° ĐŋаĐŋĐēа", "move_to_locked_folder": "ĐŸŅ€ĐĩĐŧĐĩŅŅ‚Đ¸ в СаĐēĐģŅŽŅ‡ĐĩĐŊа ĐŋаĐŋĐēа", "move_to_locked_folder_confirmation": "ĐĸĐĩСи ҁĐŊиĐŧĐēи и видĐĩа ҉Đĩ ĐąŅŠĐ´Đ°Ņ‚ Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚Đ¸ ĐžŅ‚ Đ˛ŅĐ¸Ņ‡Đēи аĐģĐąŅƒĐŧи и ҉Đĩ ŅĐ° Đ´ĐžŅŅ‚ŅŠĐŋĐŊи ŅĐ°ĐŧĐž в СаĐēĐģŅŽŅ‡ĐĩĐŊĐ°Ņ‚Đ° ĐŋаĐŋĐēа", "moved_to_archive": "{count, plural, one {# ОйĐĩĐēŅ‚ Đĩ ĐŋŅ€ĐĩĐŧĐĩҁ҂ĐĩĐŊ} many {# ОйĐĩĐēŅ‚Đ° ŅĐ° ĐŋŅ€ĐĩĐŧĐĩҁ҂ĐĩĐŊи} other {# ОйĐĩĐēŅ‚Đ° ŅĐ° ĐŋŅ€ĐĩĐŧĐĩҁ҂ĐĩĐŊи}} в Đ°Ņ€Ņ…Đ¸Đ˛Đ°", @@ -1495,7 +1520,9 @@ "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": "ĐŸŅ€ĐĩĐŧĐ°Ņ…ĐŊи ĐžŅ‚ ҁĐŋОдĐĩĐģĐĩĐŊĐ¸Ņ ĐģиĐŊĐē", @@ -1667,6 +1694,7 @@ "settings_saved": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēĐ¸Ņ‚Đĩ ŅĐ° СаĐŋаСĐĩĐŊи", "setup_pin_code": "Задай PIN ĐēОд", "share": "ĐĄĐŋОдĐĩĐģŅĐŊĐĩ", + "share_action_prompt": "{count} ҁĐŋОдĐĩĐģĐĩĐŊи ОйĐĩĐēŅ‚Đ°", "share_add_photos": "Добави ҁĐŊиĐŧĐēи", "share_assets_selected": "{count} Đ¸ĐˇĐąŅ€Đ°ĐŊи", "share_dialog_preparing": "ĐŸĐžĐ´ĐŗĐžŅ‚ĐžĐ˛Đēа...", @@ -1768,6 +1796,7 @@ "sort_title": "Đ—Đ°ĐŗĐģавиĐĩ", "source": "Код", "stack": "ĐĄŅŠĐąĐĩŅ€Đ¸", + "stack_action_prompt": "{count} ŅĐ° ĐŗŅ€ŅƒĐŋĐ¸Ņ€Đ°ĐŊи", "stack_duplicates": "ĐŸĐžĐ´Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Đ¸", "stack_select_one_photo": "ИСйĐĩŅ€Đ¸ ĐĩĐ´ĐŊа ĐŗĐģавĐŊа ҁĐŊиĐŧĐēа Са ŅŅŠĐąŅ€Đ°ĐŊĐ¸Ņ‚Đĩ ҁĐŊиĐŧĐēи", "stack_selected_photos": "ĐŸĐžĐ´Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа Đ¸ĐˇĐąŅ€Đ°ĐŊи ҁĐŊиĐŧĐēи", @@ -1838,6 +1867,7 @@ "total": "ĐžĐąŅ‰Đž", "total_usage": "ĐžĐąŅ‰Đž иСĐŋĐžĐģСваĐŊĐž", "trash": "ĐšĐžŅˆŅ‡Đĩ", + "trash_action_prompt": "{count} ŅĐ° ĐŋŅ€ĐĩĐŧĐĩҁ҂ĐĩĐŊи в ĐēĐžŅˆĐ°", "trash_all": "Đ˜ĐˇŅ…Đ˛ŅŠŅ€Đģи Đ˛ŅĐ¸Ņ‡Đēи", "trash_count": "В ĐšĐžŅˆŅ‡ĐĩŅ‚Đž {count, number}", "trash_delete_asset": "ВĐēĐ°Ņ€Đ°Đš в ĐšĐžŅˆŅ‡ĐĩŅ‚Đž/Đ˜ĐˇŅ‚Ņ€Đ¸Đš ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", @@ -1855,9 +1885,11 @@ "unable_to_change_pin_code": "НĐĩĐ˛ŅŠĐˇĐŧĐžĐļĐŊа ĐŋŅ€ĐžĐŧŅĐŊа ĐŊа PIN ĐēОда", "unable_to_setup_pin_code": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž СадаваĐŊĐĩ ĐŊа PIN ĐēОда", "unarchive": "Đ Đ°ĐˇĐ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°Đš", + "unarchive_action_prompt": "{count} ŅĐ° ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐ°Ņ‚Đ¸ ĐžŅ‚ ĐŅ€Ņ…Đ¸Đ˛Đ°", "unarchived_count": "{count, plural, other {НĐĩĐ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°ĐŊи #}}", "undo": "ĐžŅ‚ĐŧĐĩĐŊи", "unfavorite": "ĐŸŅ€ĐĩĐŧĐ°Ņ…Đ˛Đ°ĐŊĐĩ ĐžŅ‚ ĐģŅŽĐąĐ¸ĐŧĐ¸Ņ‚Đĩ", + "unfavorite_action_prompt": "{count} ŅĐ° ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐ°Ņ‚Đ¸ ĐžŅ‚ Đ›ŅŽĐąĐ¸Đŧи", "unhide_person": "ПоĐēаĐļи ĐžŅ‚ĐŊОвО Ņ‡ĐžĐ˛ĐĩĐēа", "unknown": "НĐĩиСвĐĩҁ҂ĐŊĐž", "unknown_country": "НĐĩĐŋОСĐŊĐ°Ņ‚Đ° Đ”ŅŠŅ€Đļава", @@ -1875,7 +1907,9 @@ "unselect_all_duplicates": "ĐžŅ‚ ĐŧĐ°Ņ€ĐēĐ¸Ņ€Đ°Đš Đ˛ŅĐ¸Ņ‡Đēи Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Đ¸", "unselect_all_in": "ĐŸŅ€ĐĩĐŧĐ°Ņ…ĐŊи Đ¸ĐˇĐąĐžŅ€Đ° ĐŊа Đ˛ŅĐ¸Ņ‡Đēи ĐžŅ‚ ĐŗŅ€ŅƒĐŋĐ°Ņ‚Đ° {group}", "unstack": "РаСĐēĐ°Ņ‡Đ¸", + "unstack_action_prompt": "{count} ŅĐ° Ņ€Đ°ĐˇĐŗŅ€ŅƒĐŋĐ¸Ņ€Đ°ĐŊи", "unstacked_assets_count": "РаСĐēĐ°Ņ‡ĐĩĐŊи {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸}}", + "untagged": "НĐĩĐŧĐ°Ņ€ĐēĐ¸Ņ€Đ°ĐŊи", "up_next": "ĐĄĐģĐĩĐ´Đ˛Đ°Ņ‰", "updated_at": "ОбĐŊОвĐĩĐŊĐž", "updated_password": "ĐŸĐ°Ņ€ĐžĐģĐ°Ņ‚Đ° Đĩ аĐēŅ‚ŅƒĐ°ĐģĐ¸ĐˇĐ¸Ņ€Đ°ĐŊа", diff --git a/i18n/bn.json b/i18n/bn.json index ed108652b7..d71e4e25ae 100644 --- a/i18n/bn.json +++ b/i18n/bn.json @@ -8,6 +8,7 @@ "actions": "āĻ•āĻ°ā§āĻŽ", "active": "āϏāϚāϞ", "activity": "āĻ•āĻžāĻ°ā§āϝāĻ•āϞāĻžāĻĒ", + "activity_changed": "āĻāĻ•āϟāĻŋāĻ­āĻŋāϟāĻŋ āĻāĻ–āύ {enabled, select, true {āϚāĻžāϞ⧁} other {āĻŦāĻ¨ā§āϧ}} āφāϛ⧇", "add": "āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_a_description": "āĻāĻ•āϟāĻŋ āĻŦāĻŋāĻŦāϰāĻŖ āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_a_location": "āĻāĻ•āϟāĻŋ āĻ…āĻŦāĻ¸ā§āĻĨāĻžāύ āϝ⧋āĻ— āĻ•āϰ⧁āύ", @@ -15,5 +16,84 @@ "add_a_title": "āĻāĻ•āϟāĻŋ āĻļāĻŋāϰ⧋āύāĻžāĻŽ āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_endpoint": "āĻāĻ¨ā§āĻĄāĻĒāϝāĻŧ⧇āĻ¨ā§āϟ āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_exclusion_pattern": "āĻŦāĻšāĻŋāĻ°ā§āĻ­ā§‚āϤāĻ•āϰāĻŖ āύāĻŽā§āύāĻž", - "add_url": "āϞāĻŋāĻ™ā§āĻ• āϝ⧋āĻ— āĻ•āϰ⧁āύ" + "add_import_path": "āχāĻŽāĻĒā§‹āĻ°ā§āϟ āĻ•āϰāĻžāϰ āĻĒāĻžāĻĨ āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ", + "add_location": "āĻ…āĻŦāĻ¸ā§āĻĨāĻžāύ āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ", + "add_more_users": "āφāϰ⧋ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ", + "add_partner": "āĻ…āĻ‚āĻļā§€āĻĻāĻžāϰ āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "add_path": "āĻĒāĻžāĻĨ āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ", + "add_photos": "āĻ›āĻŦāĻŋ āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ", + "add_tag": "āĻŸā§āϝāĻžāĻ— āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ", + "add_to": "āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύâ€Ļ", + "add_to_album": "āĻāϞāĻŦāĻžāĻŽ āĻ āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "add_to_album_bottom_sheet_added": "{album} āĻ āϝ⧋āĻ— āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "add_to_album_bottom_sheet_already_exists": "{album} āĻ āφāϗ⧇ āĻĨ⧇āϕ⧇āχ āφāϛ⧇", + "add_to_shared_album": "āĻļ⧇āϝāĻŧāĻžāϰ āĻ•āϰāĻž āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡ āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "add_url": "āϞāĻŋāĻ™ā§āĻ• āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "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_settings": "āĻĄāĻžāϟāĻžāĻŦ⧇āϏ āĻĄāĻžāĻŽā§āĻĒ āϏ⧇āϟāĻŋāĻ‚āϏ", + "backup_settings_description": "āĻĄāĻžāϟāĻžāĻŦ⧇āϏ āĻĄāĻžāĻŽā§āĻĒ āϏ⧇āϟāĻŋāĻ‚āϏ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύāĨ¤", + "cleared_jobs": "{job} āĻāϰ āϜāĻ¨ā§āϝ jobs āĻ–āĻžāϞāĻŋ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "config_set_by_file": "āĻ•āύāĻĢāĻŋāĻ— āĻŦāĻ°ā§āϤāĻŽāĻžāύ⧇ āĻāĻ•āϟāĻŋ āĻ•āύāĻĢāĻŋāĻ— āĻĢāĻžāχāϞ āĻĻā§āĻŦāĻžāϰāĻž āϏ⧇āϟ āĻ•āϰāĻž āφāϛ⧇", + "confirm_delete_library": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ {library} āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āϚāĻžāύ?", + "confirm_delete_library_assets": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ āĻāχ āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋāϟāĻŋ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āϚāĻžāύ? āĻāϟāĻŋ Immich āĻĨ⧇āϕ⧇ {count, plural, one {# contained asset} other {all # contained asset}} āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻŦ⧇ āĻāĻŦāĻ‚ āĻĒā§‚āĻ°ā§āĻŦāĻžāĻŦāĻ¸ā§āĻĨāĻžāϝāĻŧ āĻĢ⧇āϰāĻžāύ⧋ āϝāĻžāĻŦ⧇ āύāĻžāĨ¤ āĻĢāĻžāχāϞāϗ⧁āϞāĻŋ āĻĄāĻŋāĻ¸ā§āϕ⧇ āĻĨāĻžāĻ•āĻŦ⧇āĨ¤", + "confirm_email_below": "āύāĻŋāĻļā§āϚāĻŋāϤ āĻ•āϰāϤ⧇, āύāĻŋāĻšā§‡ \"{email}\" āϟāĻžāχāĻĒ āĻ•āϰ⧁āύ", + "confirm_reprocess_all_faces": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ āϏāĻŽāĻ¸ā§āϤ āĻŽā§āĻ– āĻĒ⧁āύāϰāĻžāϝāĻŧ āĻĒā§āϰāĻ•ā§āϰāĻŋāϝāĻŧāĻž āĻ•āϰāϤ⧇ āϚāĻžāύ? āĻāϟāĻŋ āύāĻžāĻŽāϝ⧁āĻ•ā§āϤ āĻŦā§āϝāĻ•ā§āϤāĻŋāĻĻ⧇āϰāĻ“ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻŦ⧇āĨ¤", + "confirm_user_password_reset": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ {user} āĻāϰ āĻĒāĻžāϏāĻ“āϝāĻŧāĻžāĻ°ā§āĻĄ āϰāĻŋāϏ⧇āϟ āĻ•āϰāϤ⧇ āϚāĻžāύ?", + "confirm_user_pin_code_reset": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ {user} āĻāϰ āĻĒāĻŋāύ āϕ⧋āĻĄ āϰāĻŋāϏ⧇āϟ āĻ•āϰāϤ⧇ āϚāĻžāύ?", + "create_job": "job āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ", + "cron_expression": "āĻ•ā§āϰ⧋āύ āĻāĻ•ā§āϏāĻĒā§āϰ⧇āĻļāύ", + "cron_expression_description": "āĻ•ā§āϰ⧋āύ āĻĢāĻ°ā§āĻŽā§āϝāĻžāϟ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧇ āĻ¸ā§āĻ•ā§āϝāĻžāύāĻŋāĻ‚ āĻŦā§āϝāĻŦāϧāĻžāύ āϏ⧇āϟ āĻ•āϰ⧁āύāĨ¤ āφāϰāĻ“ āϤāĻĨā§āϝ⧇āϰ āϜāĻ¨ā§āϝ āĻĻāϝāĻŧāĻž āĻ•āϰ⧇ āĻĻ⧇āϖ⧁āύ āϝ⧇āĻŽāύ Crontab Guru", + "cron_expression_presets": "āĻ•ā§āϰ⧋āύ āĻāĻ•ā§āϏāĻĒā§āϰ⧇āĻļāύ āĻĒā§āϰāĻŋāϏ⧇āϟ", + "disable_login": "āϞāĻ—āχāύ āĻ…āĻ•ā§āώāĻŽ āĻ•āϰ⧁āύ", + "duplicate_detection_job_description": "āĻ…āύ⧁āϰ⧂āĻĒ āĻ›āĻŦāĻŋ āϏāύāĻžāĻ•ā§āϤ āĻ•āϰāϤ⧇ āϏāĻŽā§āĻĒāĻĻāϗ⧁āϞāĻŋāϤ⧇ āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚ āϚāĻžāϞāĻžāύāĨ¤ āĻ¸ā§āĻŽāĻžāĻ°ā§āϟ āĻ…āύ⧁āϏāĻ¨ā§āϧāĻžāύ⧇āϰ āωāĻĒāϰ āύāĻŋāĻ°ā§āĻ­āϰ āĻ•āϰ⧇", + "exclusion_pattern_description": "āĻāĻ•ā§āϏāĻ•ā§āϞ⧁āĻļāύ āĻĒā§āϝāĻžāϟāĻžāĻ°ā§āύ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧇ āφāĻĒāύāĻŋ āφāĻĒāύāĻžāϰ āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āĻ¸ā§āĻ•ā§āϝāĻžāύ āĻ•āϰāĻžāϰ āϏāĻŽāϝāĻŧ āĻĢāĻžāχāϞ āĻāĻŦāĻ‚ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰāϗ⧁āϞāĻŋāϕ⧇ āωāĻĒ⧇āĻ•ā§āώāĻž āĻ•āϰāϤ⧇ āĻĒāĻžāϰāĻŦ⧇āύāĨ¤ āϝāĻĻāĻŋ āφāĻĒāύāĻžāϰ āĻāĻŽāύ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āĻĨāĻžāϕ⧇ āϝ⧇āĻ–āĻžāύ⧇ āĻāĻŽāύ āĻĢāĻžāχāϞ āĻĨāĻžāϕ⧇ āϝāĻž āφāĻĒāύāĻŋ āφāĻŽāĻĻāĻžāύāĻŋ āĻ•āϰāϤ⧇ āϚāĻžāύ āύāĻž, āϝ⧇āĻŽāύ RAW āĻĢāĻžāχāϞāĨ¤", + "external_library_management": "āĻŦāĻšāĻŋāϰāĻžāĻ—āϤ āĻ—ā§āϰāĻ¨ā§āĻĨāĻžāĻ—āĻžāϰ āĻŦā§āϝāĻŦāĻ¸ā§āĻĨāĻžāĻĒāύāĻž", + "face_detection": "āĻŽā§āĻ– āϏāύāĻžāĻ•ā§āϤāĻ•āϰāĻŖ", + "face_detection_description": "āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧇ āĻ…ā§āϝāĻžāϏ⧇āĻŸā§‡ āĻĨāĻžāĻ•āĻž āĻŽā§āĻ–āϗ⧁āϞāĻŋ āϏāύāĻžāĻ•ā§āϤ āĻ•āϰ⧁āύāĨ¤ āĻ­āĻŋāĻĄāĻŋāĻ“āϗ⧁āϞāĻŋāϰ āϜāĻ¨ā§āϝ, āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ āĻŦāĻŋāĻŦ⧇āϚāύāĻž āĻ•āϰāĻž āĻšāϝāĻŧāĨ¤ \"āϰāĻŋāĻĢā§āϰ⧇āĻļ\" (āĻĒ⧁āύāϰāĻžāϝāĻŧ) āϏāĻŽāĻ¸ā§āϤ āĻ…ā§āϝāĻžāϏ⧇āϟ āĻĒā§āϰāĻ•ā§āϰāĻŋāϝāĻŧāĻž āĻ•āϰ⧇āĨ¤ \"āϰāĻŋāϏ⧇āϟ\" āĻ…āϤāĻŋāϰāĻŋāĻ•ā§āϤāĻ­āĻžāĻŦ⧇ āϏāĻŽāĻ¸ā§āϤ āĻŦāĻ°ā§āϤāĻŽāĻžāύ āĻŽā§āϖ⧇āϰ āĻĄā§‡āϟāĻž āϏāĻžāĻĢ āĻ•āϰ⧇āĨ¤ \"āĻ…āύ⧁āĻĒāĻ¸ā§āĻĨāĻŋāϤ\" āĻ…ā§āϝāĻžāϏ⧇āϟāϗ⧁āϞāĻŋāϕ⧇ āϏāĻžāϰāĻŋāĻŦāĻĻā§āϧ āĻ•āϰ⧇ āϝāĻž āĻāĻ–āύāĻ“ āĻĒā§āϰāĻ•ā§āϰāĻŋāϝāĻŧāĻž āĻ•āϰāĻž āĻšāϝāĻŧāύāĻŋāĨ¤ āϏāύāĻžāĻ•ā§āϤ āĻ•āϰāĻž āĻŽā§āĻ–āϗ⧁āϞāĻŋāϕ⧇ āĻĢ⧇āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻ•āĻ—āύāĻŋāĻļāύ⧇āϰ āϜāĻ¨ā§āϝ āϏāĻžāϰāĻŋāĻŦāĻĻā§āϧ āĻ•āϰāĻž āĻšāĻŦ⧇, āĻĢ⧇āϏāĻŋāϝāĻŧāĻžāϞ āĻĄāĻŋāĻŸā§‡āĻ•āĻļāύ āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ āĻšāĻ“āϝāĻŧāĻžāϰ āĻĒāϰ⧇, āĻŦāĻŋāĻĻā§āϝāĻŽāĻžāύ āĻŦāĻž āύāϤ⧁āύ āĻŦā§āϝāĻ•ā§āϤāĻŋāĻĻ⧇āϰ āĻŽāĻ§ā§āϝ⧇ āĻ—ā§‹āĻˇā§āĻ ā§€āĻŦāĻĻā§āϧ āĻ•āϰ⧇āĨ¤", + "facial_recognition_job_description": "āĻļāύāĻžāĻ•ā§āϤ āĻ•āϰāĻž āĻŽā§āĻ–āϗ⧁āϞāĻŋāϕ⧇ āĻŽāĻžāύ⧁āώ⧇āϰ āĻŽāĻ§ā§āϝ⧇ āĻ—ā§‹āĻˇā§āĻ ā§€āϭ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύāĨ¤ āĻŽā§āĻ– āϏāύāĻžāĻ•ā§āϤāĻ•āϰāĻŖ āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ āĻšāĻ“āϝāĻŧāĻžāϰ āĻĒāϰ⧇ āĻāχ āϧāĻžāĻĒāϟāĻŋ āϚāϞ⧇āĨ¤ \"āϰāĻŋāϏ⧇āϟ\" (āĻĒ⧁āύāϰāĻžāϝāĻŧ) āϏāĻŽāĻ¸ā§āϤ āĻŽā§āĻ–āϕ⧇ āĻ•ā§āϞāĻžāĻ¸ā§āϟāĻžāϰ āĻ•āϰ⧇āĨ¤ \"āĻ…āύ⧁āĻĒāĻ¸ā§āĻĨāĻŋāϤ\" āĻŽā§āĻ–āϗ⧁āϞāĻŋāϕ⧇ āϏāĻžāϰāĻŋāϤ⧇ āϰāĻžāϖ⧇ āϝ⧇āĻ–āĻžāύ⧇ āϕ⧋āύāĻ“ āĻŦā§āϝāĻ•ā§āϤāĻŋāϕ⧇ āĻŦāϰāĻžāĻĻā§āĻĻ āĻ•āϰāĻž āĻšāϝāĻŧāύāĻŋāĨ¤", + "failed_job_command": "āĻ•āĻŽāĻžāĻ¨ā§āĻĄ {command} āĻ•āĻžāĻœā§‡āϰ āϜāĻ¨ā§āϝ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇: {job}", + "force_delete_user_warning": "āϏāϤāĻ°ā§āĻ•āϤāĻž: āĻāϟāĻŋ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āĻāĻŦāĻ‚ āϏāĻŽāĻ¸ā§āϤ āϏāĻŽā§āĻĒāĻĻ āĻ…āĻŦāĻŋāϞāĻŽā§āĻŦ⧇ āϏāϰāĻŋāϝāĻŧ⧇ āĻĢ⧇āϞāĻŦ⧇āĨ¤ āĻāϟāĻŋ āĻĒā§‚āĻ°ā§āĻŦāĻžāĻŦāĻ¸ā§āĻĨāĻžāϝāĻŧ āĻĢ⧇āϰāĻžāύ⧋ āϝāĻžāĻŦ⧇ āύāĻž āĻāĻŦāĻ‚ āĻĢāĻžāχāϞāϗ⧁āϞāĻŋ āĻĒ⧁āύāϰ⧁āĻĻā§āϧāĻžāϰ āĻ•āϰāĻž āϝāĻžāĻŦ⧇ āύāĻžāĨ¤", + "image_format": "āĻĢāϰāĻŽā§āϝāĻžāϟ", + "image_format_description": "WebP JPEG āĻāϰ āϤ⧁āϞāύāĻžā§Ÿ āϛ⧋āϟ āĻĢāĻžāχāϞ āϤ⧈āϰāĻŋ āĻ•āϰ⧇, āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻāύāϕ⧋āĻĄ āĻ•āϰāϤ⧇ āϧ⧀āϰāĨ¤", + "image_fullsize_description": "āϜ⧁āĻŽ āχāύ āĻ•āϰāĻžāϰ āϏāĻŽāϝāĻŧ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āĻ¸ā§āĻŸā§āϰāĻŋāĻĒāĻĄ āĻŽā§‡āϟāĻžāĻĄā§‡āϟāĻž āϏāĻš āĻĒā§‚āĻ°ā§āĻŖ āφāĻ•āĻžāϰ⧇āϰ āĻ›āĻŦāĻŋ", + "image_fullsize_enabled": "āĻĒā§‚āĻ°ā§āĻŖ-āφāĻ•āĻžāϰ⧇āϰ āĻ›āĻŦāĻŋ āϤ⧈āϰāĻŋ āϏāĻ•ā§āώāĻŽ āĻ•āϰ⧁āύ", + "image_fullsize_enabled_description": "āĻ“āϝāĻŧ⧇āĻŦ-āĻŦāĻžāĻ¨ā§āϧāĻŦ āύāϝāĻŧ āĻāĻŽāύ āĻĢāĻ°ā§āĻŽā§āϝāĻžāĻŸā§‡āϰ āϜāĻ¨ā§āϝ āĻĒā§‚āĻ°ā§āĻŖ-āφāĻ•āĻžāϰ⧇āϰ āĻ›āĻŦāĻŋ āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύāĨ¤ \"āĻāĻŽāĻŦ⧇āĻĄā§‡āĻĄ āĻĒā§āϰāĻŋāĻ­āĻŋāω āĻĒāĻ›āĻ¨ā§āĻĻ āĻ•āϰ⧁āύ\" āϏāĻ•ā§āώāĻŽ āĻ•āϰāĻž āĻĨāĻžāĻ•āϞ⧇, āϰ⧂āĻĒāĻžāĻ¨ā§āϤāϰ āĻ›āĻžāĻĄāĻŧāĻžāχ āĻāĻŽāĻŦ⧇āĻĄā§‡āĻĄ āĻĒā§āϰāĻŋāĻ­āĻŋāω āϏāϰāĻžāϏāϰāĻŋ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāĻž āĻšāϝāĻŧāĨ¤ JPEG-āĻāϰ āĻŽāϤ⧋ āĻ“āϝāĻŧ⧇āĻŦ-āĻŦāĻžāĻ¨ā§āϧāĻŦ āĻĢāĻ°ā§āĻŽā§āϝāĻžāϟāϗ⧁āϞāĻŋāϕ⧇ āĻĒā§āϰāĻ­āĻžāĻŦāĻŋāϤ āĻ•āϰ⧇ āύāĻžāĨ¤", + "image_fullsize_quality_description": "āĻĒā§‚āĻ°ā§āĻŖ-āφāĻ•āĻžāϰ⧇āϰ āĻ›āĻŦāĻŋāϰ āĻŽāĻžāύ ā§§-ā§§ā§Ļā§ĻāĨ¤ āωāĻšā§āϚāϤāϰ āĻšāϞ⧇ āĻ­āĻžāϞ⧋, āĻ•āĻŋāĻ¨ā§āϤ⧁ āφāϰāĻ“ āĻŦāĻĄāĻŧ āĻĢāĻžāχāϞ āϤ⧈āϰāĻŋ āĻšāϝāĻŧāĨ¤", + "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": "ā§§-ā§§ā§Ļā§Ļ āĻāϰ āĻŽāĻ§ā§āϝ⧇ āĻĒā§āϰāĻŋāĻ­āĻŋāω āϕ⧋āϝāĻŧāĻžāϞāĻŋāϟāĻŋāĨ¤ āĻŦ⧇āĻļāĻŋ āĻšāϞ⧇ āĻ­āĻžāϞ⧋, āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻŦāĻĄāĻŧ āĻĢāĻžāχāϞ āϤ⧈āϰāĻŋ āĻšāϝāĻŧ āĻāĻŦāĻ‚ āĻ…ā§āϝāĻžāĻĒ⧇āϰ āĻĒā§āϰāϤāĻŋāĻ•ā§āϰāĻŋāϝāĻŧāĻžāĻļā§€āϞāϤāĻž āĻ•āĻŽāĻžāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤ āĻ•āĻŽ āĻŽāĻžāύ āϏ⧇āϟ āĻ•āϰāϞ⧇ āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚ āϕ⧋āϝāĻŧāĻžāϞāĻŋāϟāĻŋāϰ āωāĻĒāϰ āĻĒā§āϰāĻ­āĻžāĻŦ āĻĒāĻĄāĻŧāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "image_preview_title": "āĻĒā§āϰāĻŋāĻ­āĻŋāω āϏ⧇āϟāĻŋāĻ‚āϏ", + "image_quality": "āϗ⧁āĻŖāĻŽāĻžāύ", + "image_resolution": "āϰ⧇āĻœā§‹āϞāĻŋāωāĻļāύ", + "image_resolution_description": "āωāĻšā§āϚ āϰ⧇āĻœā§‹āϞāĻŋāωāĻļāύ⧇āϰ āĻ•ā§āώ⧇āĻ¤ā§āϰ⧇ āφāϰāĻ“ āĻŦāĻŋāĻ¸ā§āϤāĻžāϰāĻŋāϤ āϤāĻĨā§āϝ āϏāĻ‚āϰāĻ•ā§āώāĻŖ āĻ•āϰāĻž āϏāĻŽā§āĻ­āĻŦ āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻāύāϕ⧋āĻĄ āĻ•āϰāϤ⧇ āĻŦ⧇āĻļāĻŋ āϏāĻŽāϝāĻŧ āϞāĻžāϗ⧇, āĻĢāĻžāχāϞ⧇āϰ āφāĻ•āĻžāϰ āĻŦāĻĄāĻŧ āĻšāϝāĻŧ āĻāĻŦāĻ‚ āĻ…ā§āϝāĻžāĻĒ⧇āϰ āĻĒā§āϰāϤāĻŋāĻ•ā§āϰāĻŋāϝāĻŧāĻžāĻļā§€āϞāϤāĻž āĻ•āĻŽāĻžāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "image_settings": "āϚāĻŋāĻ¤ā§āϰ āϏ⧇āϟāĻŋāĻ‚āϏ", + "image_settings_description": "āϤ⧈āϰāĻŋ āĻ•āϰāĻž āĻ›āĻŦāĻŋāϰ āĻŽāĻžāύ āĻāĻŦāĻ‚ āϰ⧇āĻœā§‹āϞāĻŋāωāĻļāύ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ", + "image_thumbnail_description": "āĻŽā§‡āϟāĻžāĻĄā§‡āϟāĻž āĻŦāĻžāĻĻ āĻĻ⧇āĻ“ā§ŸāĻž āϛ⧋āϟ āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ, āĻŽā§‚āϞ āϟāĻžāχāĻŽāϞāĻžāχāύ⧇āϰ āĻŽāϤ⧋ āĻ›āĻŦāĻŋāϰ āĻ—ā§āϰ⧁āĻĒ āĻĻ⧇āĻ–āĻžāϰ āϏāĻŽāϝāĻŧ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āĻšā§Ÿ", + "image_thumbnail_quality_description": "āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ⧇āϰ āĻŽāĻžāύ ā§§-ā§§ā§Ļā§ĻāĨ¤ āĻŦ⧇āĻļāĻŋ āĻšāϞ⧇ āĻ­āĻžāϞ⧋, āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻŦāĻĄāĻŧ āĻĢāĻžāχāϞ āϤ⧈āϰāĻŋ āĻšāϝāĻŧ āĻāĻŦāĻ‚ āĻ…ā§āϝāĻžāĻĒ⧇āϰ āĻĒā§āϰāϤāĻŋāĻ•ā§āϰāĻŋāϝāĻŧāĻžāĻļā§€āϞāϤāĻž āĻ•āĻŽāĻžāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "image_thumbnail_title": "āĻĨāĻžāĻŽā§āĻŦāύ⧇āϞ āϏ⧇āϟāĻŋāĻ‚āϏ", + "job_concurrency": "{job} āĻ•āύāĻ•āĻžāϰ⧇āĻ¨ā§āϏāĻŋ", + "job_created": "Job āϤ⧈āϰāĻŋ āĻšāϝāĻŧ⧇āϛ⧇", + "job_not_concurrency_safe": "āĻāχ āĻ•āĻžāϜāϟāĻŋ āϏāĻŽāĻ•āĻžāϞ⧀āύ-āύāĻŋāϰāĻžāĻĒāĻĻ āύāϝāĻŧāĨ¤", + "job_settings": "āĻ•āĻžāĻœā§‡āϰ āϏ⧇āϟāĻŋāĻ‚āϏ", + "job_settings_description": "āĻ•āĻžāĻœā§‡āϰ āϏāĻŽāĻžāĻ¨ā§āϤāϰāĻžāϞāϤāĻž āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ", + "job_status": "āϚāĻžāĻ•āϰāĻŋāϰ āĻ…āĻŦāĻ¸ā§āĻĨāĻž" + } } diff --git a/i18n/ca.json b/i18n/ca.json index 5c53311a02..39c249c3a3 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -166,6 +166,10 @@ "metadata_settings_description": "Administrar la configuraciÃŗ de les metadades", "migration_job": "MigraciÃŗ", "migration_job_description": "Migra les miniatures d'elements i cares cap a la nova estructura de carpetes", + "nightly_tasks_cluster_new_faces_setting": "Agrupa cares noves", + "nightly_tasks_database_cleanup_setting": "Tasques de neteja de la base de dades", + "nightly_tasks_database_cleanup_setting_description": "Netegeu les dades antigues i caducades de la base de dades", + "nightly_tasks_missing_thumbnails_setting": "Generar les miniatures restants", "no_paths_added": "No s'ha afegit cap ruta", "no_pattern_added": "Cap patrÃŗ aplicat", "note_apply_storage_label_previous_assets": "Nota: Per aplicar l'etiquetatge d'emmagatzematge a elements pujats prèviament, executeu la", @@ -196,6 +200,8 @@ "oauth_mobile_redirect_uri": "URI de redirecciÃŗ mÃ˛bil", "oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecciÃŗ mÃ˛bil", "oauth_mobile_redirect_uri_override_description": "Habilita quan el proveïdor d'OAuth no permet una URI mÃ˛bil, com ara ''{callback}''", + "oauth_role_claim": "ConcessiÃŗ de rol", + "oauth_role_claim_description": "Atorgar accÊs d'administrador automàticament segons la presència d'aquesta concessiÃŗ. La concessiÃŗ pot ser 'usuari' o 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "Gestiona la configuraciÃŗ de l'inici de sessiÃŗ OAuth", "oauth_settings_more_details": "Per a mÊs detalls sobre aquesta funcionalitat, consulteu la documentaciÃŗ.", @@ -244,6 +250,7 @@ "storage_template_migration_info": "Les extensions es convertiran a minÃēscules. Els canvis de plantilla nomÊs s'aplicaran a nous elements. Per aplicar la plantilla rectroactivament a elements pujats prèviament, executeu la {job}.", "storage_template_migration_job": "Tasca de migraciÃŗ de la plantilla d'emmagatzematge", "storage_template_more_details": "Per obtenir mÊs detalls sobre aquesta funciÃŗ, consulteu la Storage Template i les seves implications", + "storage_template_onboarding_description_v2": "Un cop habilitada, aquesta funciÃŗ organitzarà automàticament els fitxers a partir d'una plantilla definida per l'usuari. Per a mÊs informaciÃŗ, podeu consultar la documentaciÃŗ.", "storage_template_path_length": "Límit aproximat de longitud de la ruta: {length, number}/{limit, number}", "storage_template_settings": "Plantilla d'emmagatzematge", "storage_template_settings_description": "Gestiona l'estructura de les carpetes i el nom del fitxers dels elements pujats", @@ -359,7 +366,7 @@ "advanced_settings_enable_alternate_media_filter_subtitle": "Feu servir aquesta opciÃŗ per filtrar els continguts multimèdia durant la sincronitzaciÃŗ segons criteris alternatius. NomÊs proveu-ho si teniu problemes amb l'aplicaciÃŗ per detectar tots els àlbums.", "advanced_settings_enable_alternate_media_filter_title": "Utilitza el filtre de sincronitzaciÃŗ d'àlbums de dispositius alternatius", "advanced_settings_log_level_title": "Nivell de registre: {level}", - "advanced_settings_prefer_remote_subtitle": "Alguns dispositius sÃŗn molt lents en carregar miniatures dels elements del dispositiu. Activeu aquest paràmetre per carregar imatges remotes en el seu lloc.", + "advanced_settings_prefer_remote_subtitle": "Alguns dispositius sÃŗn molt lents en carregar miniatures dels elements locals. Activeu aquest paràmetre per carregar imatges remotes en el seu lloc.", "advanced_settings_prefer_remote_title": "Prefereix imatges remotes", "advanced_settings_proxy_headers_subtitle": "Definiu les capçaleres de proxy que Immich per enviar amb cada sol¡licitud de xarxa", "advanced_settings_proxy_headers_title": "Capçaleres de proxy", @@ -426,6 +433,7 @@ "app_settings": "ConfiguraciÃŗ de l'app", "appears_in": "Apareix a", "archive": "Arxiu", + "archive_action_prompt": "{count} afegit a Arxiu", "archive_or_unarchive_photo": "Arxivar o desarxivar fotografia", "archive_page_no_archived_assets": "No s'ha trobat res arxivat", "archive_page_title": "Arxiu({count})", @@ -463,7 +471,6 @@ "assets": "Elements", "assets_added_count": "{count, plural, one {Afegit un element} other {Afegits # elements}}", "assets_added_to_album_count": "{count, plural, one {Afegit un element} other {Afegits # elements}} a l'àlbum", - "assets_added_to_name_count": "{count, plural, one {S'ha afegit # recurs} other {S'han afegit # recursos}} a {hasName, select, true {{name}} other {new album}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} no es pot afegir a l'àlbum", "assets_count": "{count, plural, one {# recurs} other {# recursos}}", "assets_deleted_permanently": "{count} element(s) esborrats permanentment", @@ -702,7 +709,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Fosc", - "darkTheme": "Activa/desactiva el tema fosc", + "dark_theme": "Canviar a tema fosc", "date_after": "Data posterior a", "date_and_time": "Data i hora", "date_before": "Data anterior a", @@ -718,6 +725,7 @@ "default_locale": "LocalitzaciÃŗ predeterminada", "default_locale_description": "Format de dates i nÃēmeros segons la configuraciÃŗ del navegador", "delete": "Esborra", + "delete_action_prompt": "{count} eliminats permanentment", "delete_album": "Esborra l'àlbum", "delete_api_key_prompt": "Esteu segurs que voleu eliminar aquesta clau API?", "delete_dialog_alert": "Aquests elements seran eliminats de manera permanent d'Immich i del vostre dispositiu", @@ -798,6 +806,7 @@ "edit_key": "Edita clau", "edit_link": "Edita enllaç", "edit_location": "Edita ubicaciÃŗ", + "edit_location_action_prompt": "{count} ubicacions editades", "edit_location_dialog_title": "UbicaciÃŗ", "edit_name": "Edita el nom", "edit_people": "Edita la gent", @@ -983,6 +992,7 @@ "failed_to_load_assets": "Error carregant recursos", "failed_to_load_folder": "No s'ha pogut carregar la carpeta", "favorite": "Preferit", + "favorite_action_prompt": "{count} afegit a Favorits", "favorite_or_unfavorite_photo": "Foto preferida o no preferida", "favorites": "Preferits", "favorites_page_no_favorites": "No s'han trobat preferits", @@ -1149,6 +1159,7 @@ "locked_folder": "Carpeta bloquejada", "log_out": "Tanca la sessiÃŗ", "log_out_all_devices": "Tanqueu la sessiÃŗ de tots els dispositius", + "logged_in_as": "SessiÃŗ iniciada com a {user}", "logged_out_all_devices": "S'ha tancat la sessiÃŗ de tots els dispositius", "logged_out_device": "Dispositiu tancat", "login": "Iniciar sessiÃŗ", @@ -1244,6 +1255,7 @@ "more": "MÊs", "move": "Moure", "move_off_locked_folder": "Moure fora de la carpeta bloquejada", + "move_to_lock_folder_action_prompt": "{count} afegides a la carpeta protegida", "move_to_locked_folder": "Moure a la carpeta bloquejada", "move_to_locked_folder_confirmation": "Aquestes fotos i vídeos seran eliminades de tots els àlbums, i nomÊs podran ser vistes des de la carpeta bloquejada", "moved_to_archive": "S'han mogut {count, plural, one {# asset} other {# assets}} a l'arxiu", @@ -1494,6 +1506,7 @@ "remove_deleted_assets": "Suprimeix fitxers fora de línia", "remove_from_album": "Treu de l'àlbum", "remove_from_favorites": "Eliminar dels preferits", + "remove_from_lock_folder_action_prompt": "{count} eliminades de la carpeta protegida", "remove_from_locked_folder": "Elimina de la carpeta bloquejada", "remove_from_locked_folder_confirmation": "Segur que vols moure aquestes fotos i vídeos fora de la carpeta bloquejada? Seran visibles a la teva biblioteca.", "remove_from_shared_link": "Eliminar de l'enllaç compartit", @@ -1606,6 +1619,7 @@ "select_album_cover": "Seleccionar la portada de l'àlbum", "select_all": "Selecciona-ho tot", "select_all_duplicates": "Seleccioneu tots els duplicats", + "select_all_in": "Selecciona tot en {group}", "select_avatar_color": "Tria color de l'avatar", "select_face": "Selecciona cara", "select_featured_photo": "Selecciona foto principal", @@ -1835,6 +1849,7 @@ "total": "Total", "total_usage": "Ús total", "trash": "Paperera", + "trash_action_prompt": "{count} mogudes a la brossa", "trash_all": "Envia-ho tot a la paperera", "trash_count": "Paperera {count, number}", "trash_delete_asset": "Esborra/Elimina element", @@ -1852,9 +1867,11 @@ "unable_to_change_pin_code": "No es pot canviar el codi PIN", "unable_to_setup_pin_code": "No s'ha pogut configurar el codi PIN", "unarchive": "Desarxivar", + "unarchive_action_prompt": "{count} eliminades de l'arxiu", "unarchived_count": "{count, plural, other {# elements desarxivats}}", "undo": "Desfer", "unfavorite": "Reverteix preferit", + "unfavorite_action_prompt": "{count} eliminades de preferits", "unhide_person": "Mostra persona", "unknown": "Desconegut", "unknown_country": "País Desconegut", @@ -1870,6 +1887,7 @@ "unsaved_change": "Canvi no desat", "unselect_all": "Deselecciona-ho tot", "unselect_all_duplicates": "Desmarqueu tots els duplicats", + "unselect_all_in": "Desseleccionar tots els elements de {group}", "unstack": "Desapila", "unstacked_assets_count": "No apilat {count, plural, one {# recurs} other {# recursos}}", "up_next": "PrÃ˛xim", diff --git a/i18n/cs.json b/i18n/cs.json index 82e72cab46..80ea9674c0 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -44,7 +44,7 @@ "backup_database": "Vytvořit vÃŊpis databÃĄze", "backup_database_enable_description": "Povolit vÃŊpisy z databÃĄze", "backup_keep_last_amount": "Počet předchozích vÃŊpisů, kterÊ se mají ponechat", - "backup_settings": "Nastavení vÃŊpisu databÃĄze", + "backup_settings": "ZÃĄlohovÃĄní databÃĄze", "backup_settings_description": "SprÃĄva nastavení vÃŊpisu databÃĄze.", "cleared_jobs": "HotovÊ Ãēlohy pro: {job}", "config_set_by_file": "Konfigurace je aktuÃĄlně provÃĄděna konfiguračním souborem", @@ -166,6 +166,20 @@ "metadata_settings_description": "SprÃĄva nastavení metadat", "migration_job": "Migrace", "migration_job_description": "Migrace miniatur snímků a obličejů do nejnovějÅĄÃ­ struktury sloÅžek", + "nightly_tasks_cluster_faces_setting_description": "Spustit rozpoznÃĄvÃĄní obličeje na nově nalezenÃŊch obličejích", + "nightly_tasks_cluster_new_faces_setting": "Seskupit novÊ tvÃĄÅ™e", + "nightly_tasks_database_cleanup_setting": "Úlohy čiÅĄtění databÃĄze", + "nightly_tasks_database_cleanup_setting_description": "Vyčistit databÃĄzi od starÃŊch dat, jejichÅž platnost vyprÅĄela", + "nightly_tasks_generate_memories_setting": "VytvÃĄÅ™ení vzpomínek", + "nightly_tasks_generate_memories_setting_description": "VytvÃĄÅ™ení novÃŊch vzpomínek z poloÅžek", + "nightly_tasks_missing_thumbnails_setting": "Generovat chybějící miniatury", + "nightly_tasks_missing_thumbnails_setting_description": "Řadit poloÅžky bez miniatur do fronty pro generovÃĄní miniatur", + "nightly_tasks_settings": "Noční Ãēlohy", + "nightly_tasks_settings_description": "SprÃĄva nočních Ãēkolů", + "nightly_tasks_start_time_setting": "Čas zahÃĄjení", + "nightly_tasks_start_time_setting_description": "Čas, kdy server spustí noční Ãēlohy", + "nightly_tasks_sync_quota_usage_setting": "Synchronizace vyuÅžití kvÃŗty", + "nightly_tasks_sync_quota_usage_setting_description": "Aktualizovat kvÃŗtu ÃēloÅžiÅĄtě uÅživatele na zÃĄkladě aktuÃĄlního vyuÅžití", "no_paths_added": "Nebyly přidÃĄny ÅžÃĄdnÊ cesty", "no_pattern_added": "Nebyl přidÃĄn ÅžÃĄdnÃŊ vzor", "note_apply_storage_label_previous_assets": "Upozornění: Pro uplatnění Å títku ÃēloÅžiÅĄtě na dříve nahranÊ poloÅžky spusÅĨte", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "Mobilní přesměrovÃĄní URI", "oauth_mobile_redirect_uri_override": "Přepsat mobilní přesměrovÃĄní URI", "oauth_mobile_redirect_uri_override_description": "Povolit, pokud poskytovatel OAuth nepovoluje mobilní URI, například ''{callback}''", + "oauth_role_claim": "Deklarace Role", + "oauth_role_claim_description": "Automaticky udělit přístup sprÃĄvce na zÃĄkladě přítomnosti tÊto deklarace. Deklarace můŞe mít hodnotu 'user' nebo 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "SprÃĄva nastavení OAuth přihlÃĄÅĄení", "oauth_settings_more_details": "DalÅĄÃ­ podrobnosti o tÊto funkci naleznete v dokumentaci.", @@ -357,10 +373,12 @@ "admin_password": "Heslo sprÃĄvce", "administration": "Administrace", "advanced": "PokročilÊ", + "advanced_settings_beta_timeline_subtitle": "VyzkouÅĄejte novÊ prostředí aplikace", + "advanced_settings_beta_timeline_title": "ČasovÃĄ osa beta verze", "advanced_settings_enable_alternate_media_filter_subtitle": "Tuto moÅžnost pouÅžijte k filtrovÃĄní mÊdií během synchronizace na zÃĄkladě alternativních kritÊrií. Tuto moÅžnost vyzkouÅĄejte pouze v případě, Åže mÃĄte problÊmy s detekcí vÅĄech alb v aplikaci.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTÁLNÍ] PouŞít alternativní filtr pro synchronizaci alb zařízení", "advanced_settings_log_level_title": "Úroveň protokolovÃĄní: {level}", - "advanced_settings_prefer_remote_subtitle": "U některÃŊch zařízení je načítÃĄní miniatur z prostředků v zařízení velmi pomalÊ. Aktivujte toto nastavení, aby se místo toho načítaly vzdÃĄlenÊ obrÃĄzky.", + "advanced_settings_prefer_remote_subtitle": "U některÃŊch zařízení je načítÃĄní miniatur z lokÃĄlních prostředků velmi pomalÊ. Aktivujte toto nastavení, aby se místo toho načítaly vzdÃĄlenÊ obrÃĄzky.", "advanced_settings_prefer_remote_title": "Preferovat vzdÃĄlenÊ obrÃĄzky", "advanced_settings_proxy_headers_subtitle": "Definice hlaviček proxy serveru, kterÊ by měl Immich odesílat s kaÅždÃŊm síÅĨovÃŊm poÅžadavkem", "advanced_settings_proxy_headers_title": "Proxy hlavičky", @@ -388,6 +406,7 @@ "album_options": "MoÅžnosti alba", "album_remove_user": "Odebrat uÅživatele?", "album_remove_user_confirmation": "Opravdu chcete odebrat uÅživatele {user}?", + "album_search_not_found": "Nebyla nalezena ÅžÃĄdnÃĄ alba odpovídající vaÅĄemu hledÃĄní", "album_share_no_users": "Zřejmě jste toto album sdíleli se vÅĄemi uÅživateli, nebo nemÃĄte ÅžÃĄdnÊho uÅživatele, se kterÃŊm byste ho mohli sdílet.", "album_updated": "Album aktualizovÃĄno", "album_updated_setting_description": "DostÃĄvat e-mailovÃĄ oznÃĄmení o novÃŊch poloÅžkÃĄch sdílenÊho alba", @@ -407,6 +426,7 @@ "albums_default_sort_order": "VÃŊchozí řazení alb", "albums_default_sort_order_description": "VÃŊchozí řazení poloÅžek při vytvÃĄÅ™ení novÃŊch alb.", "albums_feature_description": "Sbírky poloÅžek, kterÊ lze sdílet s ostatními uÅživateli.", + "albums_on_device_count": "Alba v zařízení ({count})", "all": "VÅĄe", "all_albums": "VÅĄechna alba", "all_people": "VÅĄichni lidÊ", @@ -427,7 +447,8 @@ "app_settings": "Aplikace", "appears_in": "Vyskytuje se v", "archive": "Archiv", - "archive_or_unarchive_photo": "Archivovat nebo odarchivovat fotku", + "archive_action_prompt": "{count} přidanÃŊch do archivu", + "archive_or_unarchive_photo": "Přidat nebo odebrat fotku z archivu", "archive_page_no_archived_assets": "Nebyla nalezena ÅžÃĄdnÃĄ archivovanÃĄ mÊdia", "archive_page_title": "Archiv ({count})", "archive_size": "Velikost archivu", @@ -464,14 +485,13 @@ "assets": "PoloÅžky", "assets_added_count": "{count, plural, one {PřidÃĄna # poloÅžka} few {PřidÃĄny # poloÅžky} other {PřidÃĄno # poloÅžek}}", "assets_added_to_album_count": "Do alba {count, plural, one {byla přidÃĄna # poloÅžka} few {byly přidÃĄny # poloÅžky} other {bylo přidÃĄno # poloÅžek}}", - "assets_added_to_name_count": "{count, plural, one {PřidÃĄna # poloÅžka} few {PřidÃĄny # poloÅžky} other {PřidÃĄno # poloÅžek}} do {hasName, select, true {alba {name}} other {novÊho alba}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {PoloÅžku} other {PoloÅžky}} nelze přidat do alba", "assets_count": "{count, plural, one {# poloÅžka} few {# poloÅžky} other {# poloÅžek}}", "assets_deleted_permanently": "{count} poloÅžek trvale odstraněno", "assets_deleted_permanently_from_server": "{count} poloÅžek trvale odstraněno z Immich serveru", "assets_downloaded_failed": "{count, plural, one {StaÅžen # soubor - {error} souborů selhalo} few {StaÅženy # soubory - {error} souborů selhalo} other {StaÅženo # souborů - {error} souborů selhalo}}", "assets_downloaded_successfully": "{count, plural, one {ÚspÄ›ÅĄně staÅžen # soubor} few {ÚspÄ›ÅĄně staÅženy # soubory} other {ÚspÄ›ÅĄně staÅženo # souborů}}", - "assets_moved_to_trash_count": "Do koÅĄe {count, plural, one {přesunuta # poloÅžka} few {přesunuty # poloÅžky} other {přesunuto # poloÅžek}}", + "assets_moved_to_trash_count": "{count, plural, one {# poloÅžka přesunuta} few {# poloÅžky přesunuty} other {# poloÅžek přesunuto}} do koÅĄe", "assets_permanently_deleted_count": "Trvale {count, plural, one {smazÃĄna # poloÅžka} few {smazÃĄny # poloÅžky} other {smazÃĄno # poloÅžek}}", "assets_removed_count": "{count, plural, one {Odstraněna # poloÅžka} few {Odstraněny # poloÅžky} other {Odstraněno # poloÅžek}}", "assets_removed_permanently_from_device": "{count} poloÅžek trvale odstraněno z vaÅĄeho zařízení", @@ -553,6 +573,8 @@ "backup_options_page_title": "Nastavení zÃĄloh", "backup_setting_subtitle": "SprÃĄva nastavení zÃĄlohovÃĄní na pozadí a na popředí", "backward": "PozpÃĄtku", + "beta_sync": "Stav synchronizace beta verze", + "beta_sync_subtitle": "SprÃĄva novÊho systÊmu synchronizace", "biometric_auth_enabled": "BiometrickÊ ověřovÃĄní je povoleno", "biometric_locked_out": "Jste vyloučeni z biometrickÊho ověřovÃĄní", "biometric_no_options": "BiometrickÊ moÅžnosti nejsou k dispozici", @@ -570,7 +592,7 @@ "cache_settings_clear_cache_button": "Vymazat vyrovnÃĄvací paměÅĨ", "cache_settings_clear_cache_button_title": "VymaÅže vyrovnÃĄvací paměÅĨ aplikace. To vÃŊrazně ovlivní vÃŊkon aplikace, dokud se vyrovnÃĄvací paměÅĨ neobnoví.", "cache_settings_duplicated_assets_clear_button": "VYMAZAT", - "cache_settings_duplicated_assets_subtitle": "Fotografie a videa, kterÊ aplikace zařadila na černou listinu", + "cache_settings_duplicated_assets_subtitle": "Fotografie a videa, kterÊ aplikace ignoruje", "cache_settings_duplicated_assets_title": "Duplicitní poloÅžky ({count})", "cache_settings_statistics_album": "Knihovna nÃĄhledů", "cache_settings_statistics_full": "Kompletní fotografie", @@ -587,6 +609,7 @@ "cancel": "ZruÅĄit", "cancel_search": "ZruÅĄit vyhledÃĄvÃĄní", "canceled": "ZruÅĄeno", + "canceling": "RuÅĄení", "cannot_merge_people": "Nelze sloučit osoby", "cannot_undo_this_action": "Tuto akci nelze vrÃĄtit zpět!", "cannot_update_the_description": "Nelze aktualizovat popis", @@ -703,7 +726,7 @@ "daily_title_text_date": "EEEE, d. MMMM", "daily_title_text_date_year": "EEEE, d. MMMM y", "dark": "TmavÃŊ", - "darkTheme": "Přepnout tmavÃŊ motiv", + "dark_theme": "Přepnout tmavÃŊ motiv", "date_after": "Datum po", "date_and_time": "Datum a čas", "date_before": "Datum před", @@ -719,6 +742,7 @@ "default_locale": "VÃŊchozí jazyk", "default_locale_description": "FormÃĄtovat datumy a čísla podle místního prostředí prohlíŞeče", "delete": "Smazat", + "delete_action_prompt": "{count} trvale smazanÃŊch", "delete_album": "Smazat album", "delete_api_key_prompt": "Opravdu chcete tento API klíč odstranit?", "delete_dialog_alert": "Tyto poloÅžky budou trvale smazÃĄny z aplikace Immich i z vaÅĄeho zařízení", @@ -732,6 +756,7 @@ "delete_key": "Smazat klíč", "delete_library": "Smazat knihovnu", "delete_link": "Smazat odkaz", + "delete_local_action_prompt": "{count} smazÃĄno lokÃĄlně", "delete_local_dialog_ok_backed_up_only": "Smazat pouze zÃĄlohovanÊ", "delete_local_dialog_ok_force": "Přesto smazat", "delete_others": "Odstranit ostatní", @@ -745,6 +770,7 @@ "description": "Popis", "description_input_hint_text": "Přidat popis...", "description_input_submit_error": "Chyba aktualizace popisu, dalÅĄÃ­ podrobnosti najdete v logu", + "deselect_all": "ZruÅĄit vÃŊběr vÅĄech", "details": "Podrobnosti", "direction": "Směr", "disabled": "ZakÃĄzÃĄno", @@ -762,6 +788,7 @@ "documentation": "Dokumentace", "done": "Hotovo", "download": "StÃĄhnout", + "download_action_prompt": "StahovÃĄní {count} poloÅžek", "download_canceled": "StahovÃĄní zruÅĄeno", "download_complete": "StahovÃĄní kompletní", "download_enqueue": "StahovÃĄní ve frontě", @@ -799,6 +826,7 @@ "edit_key": "Upravit klíč", "edit_link": "Upravit odkaz", "edit_location": "Upravit polohu", + "edit_location_action_prompt": "{count} upravenÃŊch poloh", "edit_location_dialog_title": "Poloha", "edit_name": "Upravit jmÊno", "edit_people": "Upravit lidi", @@ -817,6 +845,7 @@ "empty_trash": "VyprÃĄzdnit koÅĄ", "empty_trash_confirmation": "Opravdu chcete vysypat koÅĄ? Tím se z Immiche trvale odstraní vÅĄechny poloÅžky v koÅĄi.\nTuto akci nelze vrÃĄtit zpět!", "enable": "Povolit", + "enable_backup": "Povolit zÃĄlohovÃĄní", "enable_biometric_auth_description": "Zadejte vÃĄÅĄ PIN kÃŗd pro povolení biometrickÊho ověřovÃĄní", "enabled": "Povoleno", "end_date": "KonečnÊ datum", @@ -861,7 +890,7 @@ "failed_to_load_people": "Chyba načítÃĄní osob", "failed_to_remove_product_key": "Nepodařilo se odebrat klíč produktu", "failed_to_stack_assets": "Nepodařilo se seskupit poloÅžky", - "failed_to_unstack_assets": "Nepodařilo se rozloÅžit poloÅžky", + "failed_to_unstack_assets": "Nepodařilo se zruÅĄit seskupení poloÅžek", "failed_to_update_notification_status": "Nepodařilo se aktualizovat stav oznÃĄmení", "import_path_already_exists": "Tato cesta importu jiÅž existuje.", "incorrect_email_or_password": "NesprÃĄvnÃŊ e-mail nebo heslo", @@ -876,7 +905,7 @@ "unable_to_add_partners": "Nelze přidat partnery", "unable_to_add_remove_archive": "Nelze {archived, select, true {odstranit poloÅžku z} other {přidat poloÅžku do}} archivu", "unable_to_add_remove_favorites": "Nelze {favorite, select, true {oblíbit poloÅžku} other {zruÅĄit oblíbení poloÅžky}}", - "unable_to_archive_unarchive": "Nelze {archived, select, true {archivovat} other {odarchivovat}}", + "unable_to_archive_unarchive": "Nelze {archived, select, true {archivovat} other {odebrat z archivu}}", "unable_to_change_album_user_role": "Nelze změnit roli uÅživatele alba", "unable_to_change_date": "Nelze změnit datum", "unable_to_change_description": "Nelze změnit popis", @@ -984,6 +1013,7 @@ "failed_to_load_assets": "Nepodařilo se načíst poloÅžky", "failed_to_load_folder": "Nepodařilo se načíst sloÅžku", "favorite": "Oblíbit", + "favorite_action_prompt": "{count} přidÃĄno do OblíbenÃŊch", "favorite_or_unfavorite_photo": "Oblíbit nebo zruÅĄit oblíbení fotky", "favorites": "OblíbenÊ", "favorites_page_no_favorites": "Nebyla nalezena ÅžÃĄdnÃĄ oblíbenÃĄ mÊdia", @@ -1023,6 +1053,9 @@ "haptic_feedback_switch": "Povolit dotykovou zpětnou vazbu", "haptic_feedback_title": "DotykovÃĄ zpětnÃĄ vazba", "has_quota": "MÃĄ kvÃŗtu", + "hash_asset": "Hash poloÅžky", + "hashed_assets": "HashovanÊ poloÅžky", + "hashing": "HashovÃĄní", "header_settings_add_header_tip": "Přidat hlavičku", "header_settings_field_validator_msg": "Hodnota nemůŞe bÃŊt prÃĄzdnÃĄ", "header_settings_header_name_input": "NÃĄzev hlavičky", @@ -1055,6 +1088,7 @@ "host": "Hostitel", "hour": "Hodina", "id": "ID", + "idle": "Nečinnost", "ignore_icloud_photos": "Ignorovat fotografie na iCloudu", "ignore_icloud_photos_description": "Fotografie uloÅženÊ na iCloudu se nebudou nahrÃĄvat na Immich server", "image": "ObrÃĄzek", @@ -1127,6 +1161,7 @@ "library_page_sort_created": "Naposledy vytvořenÊ", "library_page_sort_last_modified": "Naposledy upraveno", "library_page_sort_title": "Podle nÃĄzvu alba", + "licenses": "Licence", "light": "SvětlÃŊ", "like_deleted": "Lajk smazÃĄn", "link_motion_video": "Připojit pohyblivÊ video", @@ -1136,7 +1171,9 @@ "list": "Seznam", "loading": "NačítÃĄní", "loading_search_results_failed": "NačítÃĄní vÃŊsledků vyhledÃĄvÃĄní se nezdařilo", + "local": "Místní", "local_asset_cast_failed": "Nelze odeslat poloÅžku, kterÃĄ není nahranÃĄ na serveru", + "local_assets": "Místní poloÅžky", "local_network": "Místní síÅĨ", "local_network_sheet_info": "Aplikace se při pouÅžití zadanÊ sítě Wi-Fi připojí k serveru prostřednictvím tohoto URL", "location_permission": "OprÃĄvnění polohy", @@ -1246,10 +1283,11 @@ "more": "Více", "move": "Přesunout", "move_off_locked_folder": "Přesunout z uzamčenÊ sloÅžky", + "move_to_lock_folder_action_prompt": "{count} přidanÃŊch do uzamčenÊ sloÅžky", "move_to_locked_folder": "Přesunout do uzamčenÊ sloÅžky", "move_to_locked_folder_confirmation": "Tyto fotky a videa budou odstraněny ze vÅĄech alb a bude je moÅžnÊ zobrazit pouze v uzamčenÊ sloÅžce", - "moved_to_archive": "{count, plural, one {Přesunuta # poloÅžka} few {Přesunuty # poloÅžky} other {Přesunuto # poloÅžek}} do archivu", - "moved_to_library": "{count, plural, one {Přesunuta # poloÅžka} few {Přesunuty # poloÅžky} other {Přesunuto # poloÅžek}} do knihovny", + "moved_to_archive": "{count, plural, one {# poloÅžka přesunuta} few {# poloÅžky přesunuty} other {# poloÅžek přesunuto}} do archivu", + "moved_to_library": "{count, plural, one {# poloÅžka přesunuta} few {# poloÅžky přesunuty} other {# poloÅžek přesunuto}} do knihovny", "moved_to_trash": "Přesunuto do koÅĄe", "multiselect_grid_edit_date_time_err_read_only": "Nelze upravit datum poloÅžek pouze pro čtení, přeskakuji", "multiselect_grid_edit_gps_err_read_only": "Nelze upravit polohu poloÅžek pouze pro čtení, přeskakuji", @@ -1292,6 +1330,7 @@ "no_results": "ÅŊÃĄdnÊ vÃŊsledky", "no_results_description": "Zkuste pouŞít synonymum nebo obecnějÅĄÃ­ klíčovÊ slovo", "no_shared_albums_message": "Vytvořte si album a sdílejte fotografie a videa s lidmi ve svÊ síti", + "no_uploads_in_progress": "NeprobíhÃĄ ÅžÃĄdnÊ nahrÃĄvÃĄní", "not_in_any_album": "Bez alba", "not_selected": "Není vybrÃĄno", "note_apply_storage_label_to_previously_uploaded assets": "Upozornění: Chcete-li pouŞít ÅĄtítek ÃēloÅžiÅĄtě na dříve nahranÊ poloÅžky, spusÅĨte příkaz", @@ -1329,6 +1368,7 @@ "original": "originÃĄl", "other": "Ostatní", "other_devices": "Ostatní zařízení", + "other_entities": "Ostatní entity", "other_variables": "DalÅĄÃ­ proměnnÊ", "owned": "Vlastní", "owner": "Vlastník", @@ -1460,6 +1500,7 @@ "purchase_server_description_2": "Stav podporovatele", "purchase_server_title": "Server", "purchase_settings_server_activated": "ProduktovÃŊ klíč serveru spravuje sprÃĄvce", + "queue_status": "Ve frontě {count}/{total}", "rating": "Hodnocení hvězdičkami", "rating_clear": "Vyčistit hodnocení", "rating_count": "{count, plural, one {# hvězdička} few {# hvězdičky} other {# hvězdček}}", @@ -1488,6 +1529,8 @@ "refreshing_faces": "ObnovovÃĄní obličejů", "refreshing_metadata": "ObnovovÃĄní metadat", "regenerating_thumbnails": "Regenerace miniatur", + "remote": "VzdÃĄlenÃŊ", + "remote_assets": "VzdÃĄlenÊ poloÅžky", "remove": "Odstranit", "remove_assets_album_confirmation": "Opravdu chcete z alba odstranit {count, plural, one {# poloÅžku} few {# poloÅžky} other {# poloÅžek}}?", "remove_assets_shared_link_confirmation": "Opravdu chcete ze sdílenÊho odkazu odstranit {count, plural, one {# poloÅžku} few {# poloÅžky} other {# poloÅžek}}?", @@ -1495,7 +1538,9 @@ "remove_custom_date_range": "Odstranit vlastní rozsah datumů", "remove_deleted_assets": "Odstranit offline soubory", "remove_from_album": "Odstranit z alba", + "remove_from_album_action_prompt": "{count} odstraněnÃŊch z alba", "remove_from_favorites": "Odstranit z oblíbenÃŊch", + "remove_from_lock_folder_action_prompt": "{count} odebranÃŊch z uzamčenÊ sloÅžky", "remove_from_locked_folder": "Odstranit z uzamčenÊ sloÅžky", "remove_from_locked_folder_confirmation": "Opravdu chcete tyto fotky a videa přesunout z uzamčenÊ sloÅžky? Budou viditelnÊ ve vaÅĄÃ­ knihovně.", "remove_from_shared_link": "Odstranit ze sdílenÊho odkazu", @@ -1523,11 +1568,15 @@ "reset_password": "Obnovit heslo", "reset_people_visibility": "Obnovit viditelnost lidí", "reset_pin_code": "Resetovat PIN kÃŗd", + "reset_sqlite": "Obnovit SQLite databÃĄzi", + "reset_sqlite_confirmation": "Jste si jisti, Åže chcete obnovit SQLite databÃĄzi? Pro opětovnou synchronizaci dat se budete muset odhlÃĄsit a znovu přihlÃĄsit", + "reset_sqlite_success": "Obnovení SQLite databÃĄze proběhlo ÃēspÄ›ÅĄně", "reset_to_default": "Obnovit vÃŊchozí nastavení", "resolve_duplicates": "VyřeÅĄit duplicity", "resolved_all_duplicates": "VyřeÅĄeny vÅĄechny duplicity", "restore": "Obnovit", "restore_all": "Obnovit vÅĄe", + "restore_trash_action_prompt": "{count} obnoveno z koÅĄe", "restore_user": "Obnovit uÅživatele", "restored_asset": "PoloÅžka obnovena", "resume": "Pokračovat", @@ -1536,6 +1585,7 @@ "role": "Role", "role_editor": "Editor", "role_viewer": "DivÃĄk", + "running": "ProbíhÃĄ", "save": "UloÅžit", "save_to_gallery": "UloÅžit do galerie", "saved_api_key": "API klíč uloÅžen", @@ -1667,6 +1717,7 @@ "settings_saved": "Nastavení uloÅženo", "setup_pin_code": "Nastavení PIN kÃŗdu", "share": "Sdílet", + "share_action_prompt": "Sdíleno {count} poloÅžek", "share_add_photos": "Přidat fotografie", "share_assets_selected": "{count} vybrÃĄno", "share_dialog_preparing": "Připravuji...", @@ -1768,6 +1819,7 @@ "sort_title": "NÃĄzev alba", "source": "Zdroj", "stack": "Seskupit", + "stack_action_prompt": "{count} seskupeno", "stack_duplicates": "Seskupit duplicity", "stack_select_one_photo": "Vyberte jednu hlavní fotografii pro seskupení", "stack_selected_photos": "Seskupení vybranÃŊch fotografií", @@ -1787,6 +1839,7 @@ "storage_quota": "KvÃŗta ÃēloÅžiÅĄtě", "storage_usage": "VyuÅžito {used} z {available}", "submit": "Odeslat", + "success": "Úspěch", "suggestions": "NÃĄvrhy", "sunrise_on_the_beach": "VÃŊchod slunce na plÃĄÅži", "support": "Podpora", @@ -1796,6 +1849,8 @@ "sync": "Synchronizovat", "sync_albums": "Synchronizovat alba", "sync_albums_manual_subtitle": "Synchronizovat vÅĄechna nahranÃĄ videa a fotografie do vybranÃŊch zÃĄloÅžních alb", + "sync_local": "Synchronizovat místní", + "sync_remote": "Synchronizovat vzdÃĄlenÊ", "sync_upload_album_setting_subtitle": "Vytvořit a nahrÃĄt fotografie a videa do vybranÃŊch alb na Immich", "tag": "Značka", "tag_assets": "Přiřadit značku", @@ -1806,6 +1861,7 @@ "tag_updated": "AktualizovÃĄna značka: {tag}", "tagged_assets": "Přiřazena značka {count, plural, one {# poloÅžce} other {# poloÅžkÃĄm}}", "tags": "Značky", + "tap_to_run_job": "Klepnutím na spustíte Ãēlohu", "template": "Å ablona", "theme": "Motiv", "theme_selection": "VÃŊběr motivu", @@ -1838,6 +1894,7 @@ "total": "Celkem", "total_usage": "CelkovÊ vyuÅžití", "trash": "KoÅĄ", + "trash_action_prompt": "{count} přesunutÃŊch do koÅĄe", "trash_all": "Vyhodit vÅĄe", "trash_count": "Vyhodit {count, number}", "trash_delete_asset": "Vyhodit/Smazat poloÅžku", @@ -1854,10 +1911,12 @@ "type": "Typ", "unable_to_change_pin_code": "Nelze změnit PIN kÃŗd", "unable_to_setup_pin_code": "Nelze nastavit PIN kÃŗd", - "unarchive": "Odarchivovat", + "unarchive": "Odebrat z archivu", + "unarchive_action_prompt": "{count} odstraněnÃŊch z archivu", "unarchived_count": "{count, plural, one {OdarchivovÃĄna #} few {OdarchivovÃĄny #} other {OdarchivovÃĄno #}}", "undo": "VrÃĄtit zpět", "unfavorite": "ZruÅĄit oblíbení", + "unfavorite_action_prompt": "{count} odstraněnÃŊch z oblíbenÃŊch", "unhide_person": "ZruÅĄit skrytí osoby", "unknown": "NeznÃĄmÃŊ", "unknown_country": "NeznÃĄmÃĄ země", @@ -1875,12 +1934,15 @@ "unselect_all_duplicates": "ZruÅĄit vÃŊběr vÅĄech duplicit", "unselect_all_in": "ZruÅĄit vÃŊběr ve skupině {group}", "unstack": "ZruÅĄit seskupení", - "unstacked_assets_count": "{count, plural, one {RozloÅženÃĄ # poloÅžka} few {RozloÅženÊ # poloÅžky} other {RozloÅženÃŊch # poloÅžiek}}", + "unstack_action_prompt": "{count} seskupenÃŊch zruÅĄeno", + "unstacked_assets_count": "{count, plural, one {RozloÅženÃĄ # poloÅžka} few {RozloÅženÊ # poloÅžky} other {RozloÅženÃŊch # poloÅžek}}", + "untagged": "Neoznačeno", "up_next": "To je prozatím vÅĄe", "updated_at": "AktualizovÃĄno", "updated_password": "Heslo aktualizovÃĄno", "upload": "NahrÃĄt", "upload_concurrency": "SouběŞnost nahrÃĄvÃĄní", + "upload_details": "Detaily nahrÃĄvÃĄní", "upload_dialog_info": "Chcete zÃĄlohovat vybranÊ poloÅžky na server?", "upload_dialog_title": "NahrÃĄt poloÅžku", "upload_errors": "NahrÃĄvÃĄní bylo dokončeno s {count, plural, one {# chybou} other {# chybami}}, obnovte strÃĄnku pro zobrazení novÃŊch poloÅžek.", @@ -1912,6 +1974,7 @@ "user_usage_stats_description": "Zobrazit statistiky pouŞívÃĄní Ãēčtu", "username": "UÅživateleskÊ jmÊno", "users": "UÅživatelÊ", + "users_added_to_album_count": "{count, plural, one {PřidÃĄn # uÅživatel} few {PřidÃĄny # uÅživatelÊ} other {PřidÃĄno # uÅživatelů}} do alba", "utilities": "NÃĄstroje", "validate": "Ověřit", "validate_endpoint_error": "Zadejte platnÊ URL", @@ -1930,6 +1993,7 @@ "view_album": "Zobrazit album", "view_all": "Zobrazit vÅĄe", "view_all_users": "Zobrazit vÅĄechny uÅživatele", + "view_details": "Zobrazit podrobnosti", "view_in_timeline": "Zobrazit na časovÊ ose", "view_link": "Zobrazit odkaz", "view_links": "Zobrazit odkazy", @@ -1941,7 +2005,7 @@ "view_user": "Zobrazit uÅživatele", "viewer_remove_from_stack": "Odstranit ze zÃĄsobníku", "viewer_stack_use_as_main_asset": "PouŞít jako hlavní poloÅžku", - "viewer_unstack": "Rozbalit zÃĄsobník", + "viewer_unstack": "ZruÅĄit zÃĄsobník", "visibility_changed": "Viditelnost změněna u {count, plural, one {# osoby} few {# osob} other {# lidí}}", "waiting": "Čekající", "warning": "Upozornění", diff --git a/i18n/da.json b/i18n/da.json index 3273a2d553..e9f9534927 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -34,6 +34,7 @@ "added_to_favorites_count": "Tilføjet {count, number} til favoritter", "admin": { "add_exclusion_pattern_description": "Tilføj udelukkelsesmønstre. Globbing ved hjÃĻlp af *, ** og ? understøttes. For at ignorere alle filer i enhver mappe med navnet \"Raw\", brug \"**/Raw/**\". For at ignorere alle filer, der slutter pÃĨ \".tif\", brug \"**/*.tif\". For at ignorere en absolut sti, brug \"/sti/til/ignoreret/**\".", + "admin_user": "Administrator bruger", "asset_offline_description": "Denne eksterne biblioteksressource findes ikke lÃĻngere pÃĨ disken og er blevet flyttet til papirkurven. Hvis filen blev flyttet inde i biblioteket, skal du tjekke din tidslinje for den nye tilsvarende ressource. For at gendanne denne ressource skal du sikre, at filstien nedenfor kan tilgÃĨs af Immich og scanne biblioteket.", "authentication_settings": "Godkendelsesindstillinger", "authentication_settings_description": "Administrer adgangskode, OAuth og andre godkendelsesindstillinger", @@ -165,6 +166,7 @@ "metadata_settings_description": "HÃĨndtÊr metadataindstillinger", "migration_job": "Migrering", "migration_job_description": "MigrÊr miniaturebilleder for aktiver og ansigter til den seneste mappestruktur", + "nightly_tasks_cluster_faces_setting_description": "Kør ansigtsgenkendelse pÃĨ nye ansigter", "no_paths_added": "Ingen stier tilføjet", "no_pattern_added": "Intet mønster tilføjet", "note_apply_storage_label_previous_assets": "BemÃĻrk: For at anvende LagringsmÃĻrkatet pÃĨ tidligere uploadede mediefiler, kør", @@ -462,7 +464,6 @@ "assets": "elementer", "assets_added_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}}", "assets_added_to_album_count": "{count, plural, one {# mediefil} other {# mediefiler}} tilføjet til albummet", - "assets_added_to_name_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}} til {hasName, select, true {{name}} other {nyt album}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Billed} other {Billeder}} kan ikke blive tilføjet til album", "assets_count": "{count, plural, one {# mediefil} other {# mediefiler}}", "assets_deleted_permanently": "{count} element(er) blev fjernet permanent", @@ -701,7 +702,6 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Mørk", - "darkTheme": "Skift til mørkt tema", "date_after": "Dato efter", "date_and_time": "Dato og klokkeslÃĻt", "date_before": "Dato før", diff --git a/i18n/de.json b/i18n/de.json index bbcd9c569c..45245b182b 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -56,9 +56,9 @@ "confirm_user_pin_code_reset": "Bist du sicher, dass du den PIN Code von {user} zurÃŧcksetzen mÃļchtest?", "create_job": "Aufgabe erstellen", "cron_expression": "Cron Zeitangabe", - "cron_expression_description": "Setze ein Intervall fÃŧr die Sicherung mittels cron. Hilfe mit dem Format bietet dir dabei z.B der Crontab Guru", + "cron_expression_description": "Setze ein Intervall fÃŧr die Sicherung mittels cron. Hilfe mit dem Format bietet dir dabei z. B. der Crontab Guru", "cron_expression_presets": "NÃŧtzliche Zeitangaben fÃŧr Cron", - "disable_login": "Login deaktvieren", + "disable_login": "Login deaktivieren", "duplicate_detection_job_description": "Diese Aufgabe fÃŧhrt das maschinelle Lernen fÃŧr jede Datei aus, um Duplikate zu finden. Diese Aufgabe beruht auf der intelligenten Suche", "exclusion_pattern_description": "Mit Ausschlussmustern kÃļnnen Dateien und Ordner beim Scannen Ihrer Bibliothek ignoriert werden. Dies ist nÃŧtzlich, wenn du Ordner hast, die Dateien enthalten, die du nicht importieren mÃļchtest, wie z. B. RAW-Dateien.", "external_library_management": "Verwaltung externer Bibliotheken", @@ -166,6 +166,20 @@ "metadata_settings_description": "Metadaten-Einstellungen verwalten", "migration_job": "Migration", "migration_job_description": "Diese Aufgabe migriert Miniaturansichten fÃŧr Dateien und Gesichter in die neueste Ordnerstruktur", + "nightly_tasks_cluster_faces_setting_description": "Gesichtsidentifikation auf neu erkannten Gesichtern ausfÃŧhren", + "nightly_tasks_cluster_new_faces_setting": "Neue Gesichter gruppieren", + "nightly_tasks_database_cleanup_setting": "Datenbankbereinigungs-Aufgaben", + "nightly_tasks_database_cleanup_setting_description": "Alte, abgelaufene Daten aus der Datenbank bereinigen", + "nightly_tasks_generate_memories_setting": "Erinnerungen generieren", + "nightly_tasks_generate_memories_setting_description": "Neue Erinnerungen aus Dateien erstellen", + "nightly_tasks_missing_thumbnails_setting": "Fehlende Miniaturansichten generieren", + "nightly_tasks_missing_thumbnails_setting_description": "Dateien ohne Miniaturansicht in die Warteschlange zur Miniaturansicht-Generierung hinzufÃŧgen", + "nightly_tasks_settings": "Einstellungen fÃŧr nächtliche Aufgaben", + "nightly_tasks_settings_description": "Nächtliche Aufgaben verwalten", + "nightly_tasks_start_time_setting": "Startzeit", + "nightly_tasks_start_time_setting_description": "Die Zeit, zu der der Server mit der AusfÃŧhrung der nächtlichen Aufgaben beginnt", + "nightly_tasks_sync_quota_usage_setting": "Kontingentnutzung synchronisieren", + "nightly_tasks_sync_quota_usage_setting_description": "Benutzerspeicherkontingent basierend auf der aktuellen Nutzung aktualisieren", "no_paths_added": "Keine Pfade hinzugefÃŧgt", "no_pattern_added": "Kein Ausschlussmuster hinzugefÃŧgt", "note_apply_storage_label_previous_assets": "Hinweis: Um den Speicherpfad auf die vorher hochgeladenen Dateien anzuwenden, starte den", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "Mobile Umleitungs-URI", "oauth_mobile_redirect_uri_override": "Mobile Umleitungs-URI Ãŧberschreiben", "oauth_mobile_redirect_uri_override_description": "Einschalten, wenn der OAuth-Anbieter keine mobile URI wie ''{callback}'' erlaubt", + "oauth_role_claim": "Rollen-Claim", + "oauth_role_claim_description": "Gewähre automatisch Admin-Zugriff basierend auf dem Vorhandensein dieses Claims. Der Claim kann entweder 'user' oder 'admin' sein.", "oauth_settings": "OAuth", "oauth_settings_description": "OAuth-Anmeldeeinstellungen verwalten", "oauth_settings_more_details": "Weitere Informationen zu dieser Funktion findest du in der Dokumentation.", @@ -244,6 +260,7 @@ "storage_template_migration_info": "Die Speichervorlage wird alle Dateierweiterungen in Kleinbuchstaben umwandeln. Vorlagenänderungen gelten nur fÃŧr neue Dateien. Um die Vorlage rÃŧckwirkend auf bereits hochgeladene Assets anzuwenden, fÃŧhre den {job} aus.", "storage_template_migration_job": "Speichervorlagenmigrations-Aufgabe", "storage_template_more_details": "Weitere Details zu dieser Funktion findest du unter Speichervorlage und dessen Implikationen", + "storage_template_onboarding_description_v2": "Wenn aktiviert, werden Dateien automatisch nach einer benutzerdefinierten Vorlage organisiert. FÃŧr mehr Informationen siehe die Dokumentation.", "storage_template_path_length": "Ungefähres Pfadlängen-Limit: {length, number}/{limit, number}", "storage_template_settings": "Speichervorlage", "storage_template_settings_description": "Die Ordnerstruktur und den Dateinamen der hochgeladenen Datei verwalten", @@ -356,10 +373,12 @@ "admin_password": "Administrator Passwort", "administration": "Verwaltung", "advanced": "Erweitert", + "advanced_settings_beta_timeline_subtitle": "Probier die neue App-Erfahrung aus", + "advanced_settings_beta_timeline_title": "Beta-Timeline", "advanced_settings_enable_alternate_media_filter_subtitle": "Verwende diese Option, um Medien während der Synchronisierung nach anderen Kriterien zu filtern. Versuchen dies nur, wenn Probleme mit der Erkennung aller Alben durch die App auftreten.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTELL] Benutze alternativen Filter fÃŧr Synchronisierung der Gerätealben", "advanced_settings_log_level_title": "Log-Level: {level}", - "advanced_settings_prefer_remote_subtitle": "Einige Geräte sind sehr langsam beim Laden von Miniaturbildern direkt aus dem Gerät. Aktivieren Sie diese Einstellung, um stattdessen die Server-Bilder zu laden.", + "advanced_settings_prefer_remote_subtitle": "Einige Geräte sind sehr langsam beim Laden von lokalen Vorschaubildern. Aktivieren Sie diese Einstellung, um stattdessen die Server-Bilder zu laden.", "advanced_settings_prefer_remote_title": "Server-Bilder bevorzugen", "advanced_settings_proxy_headers_subtitle": "Definiere einen Proxy-Header, den Immich bei jeder Netzwerkanfrage mitschicken soll", "advanced_settings_proxy_headers_title": "Proxy-Headers", @@ -387,6 +406,7 @@ "album_options": "Albumoptionen", "album_remove_user": "Nutzer entfernen?", "album_remove_user_confirmation": "Bist du sicher, dass du {user} entfernen willst?", + "album_search_not_found": "Keine Alben gefunden, die zur Suche passen", "album_share_no_users": "Es sieht so aus, als hättest du dieses Album mit allen Benutzern geteilt oder du hast keine Benutzer, mit denen du teilen kannst.", "album_updated": "Album aktualisiert", "album_updated_setting_description": "Erhalte eine E-Mail-Benachrichtigung, wenn ein freigegebenes Album neue Dateien enthält", @@ -406,6 +426,7 @@ "albums_default_sort_order": "Standard Album Sortierung", "albums_default_sort_order_description": "Sortierreihenfolge der Dateien bei der Erstellung neuer Alben.", "albums_feature_description": "Sammlung an Alben die mit anderen Benutzern geteilt werden kÃļnnen.", + "albums_on_device_count": "Alben auf dem Gerät ({count})", "all": "Alle", "all_albums": "Alle Alben", "all_people": "Alle Personen", @@ -426,6 +447,7 @@ "app_settings": "App-Einstellungen", "appears_in": "Erscheint in", "archive": "Archiv", + "archive_action_prompt": "{count} zum Archiv hinzugefÃŧgt", "archive_or_unarchive_photo": "Foto archivieren bzw. Archivierung aufheben", "archive_page_no_archived_assets": "Keine archivierten Inhalte gefunden", "archive_page_title": "Archiv ({count})", @@ -463,7 +485,6 @@ "assets": "Dateien", "assets_added_count": "{count, plural, one {# Datei} other {# Dateien}} hinzugefÃŧgt", "assets_added_to_album_count": "{count, plural, one {# Datei} other {# Dateien}} zum Album hinzugefÃŧgt", - "assets_added_to_name_count": "{count, plural, one {# Element} other {# Elemente}} zu {hasName, select, true {{name}} other {neuem Album}} hinzugefÃŧgt", "assets_cannot_be_added_to_album_count": "{count, plural, one {Datei kann}other {Dateien kÃļnnen}} nicht zum Album hinzugefÃŧgt werden", "assets_count": "{count, plural, one {# Datei} other {# Dateien}}", "assets_deleted_permanently": "{count} Element(e) permanent gelÃļscht", @@ -552,6 +573,8 @@ "backup_options_page_title": "Sicherungsoptionen", "backup_setting_subtitle": "Verwaltung der Upload-Einstellungen im Hintergrund und im Vordergrund", "backward": "RÃŧckwärts", + "beta_sync": "Status des Beta Sync", + "beta_sync_subtitle": "Verwalte das neue Synchronisierungssystem", "biometric_auth_enabled": "Biometrische Authentifizierung aktiviert", "biometric_locked_out": "Du bist von der biometrischen Authentifizierung ausgeschlossen", "biometric_no_options": "Keine biometrischen Optionen verfÃŧgbar", @@ -586,6 +609,7 @@ "cancel": "Abbrechen", "cancel_search": "Suche abbrechen", "canceled": "Abgebrochen", + "canceling": "Abbrechen", "cannot_merge_people": "Personen kÃļnnen nicht zusammengefÃŧhrt werden", "cannot_undo_this_action": "Diese Aktion kann nicht rÃŧckgängig gemacht werden!", "cannot_update_the_description": "Beschreibung kann nicht aktualisiert werden", @@ -702,7 +726,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Dunkel", - "darkTheme": "Dunkles Theme umschalten", + "dark_theme": "Dunkle Ansicht umschalten", "date_after": "Datum nach", "date_and_time": "Datum und Zeit", "date_before": "Datum vor", @@ -718,6 +742,7 @@ "default_locale": "Standard-Sprache", "default_locale_description": "Datumsangaben und Zahlen basierend auf dem Gebietsschema des Browsers formatieren", "delete": "LÃļschen", + "delete_action_prompt": "{count} endgÃŧltig gelÃļscht", "delete_album": "Album lÃļschen", "delete_api_key_prompt": "Bist du sicher, dass du diesen API-SchlÃŧssel lÃļschen willst?", "delete_dialog_alert": "Diese Elemente werden unwiderruflich von Immich und dem Gerät entfernt", @@ -731,6 +756,7 @@ "delete_key": "SchlÃŧssel lÃļschen", "delete_library": "Bibliothek lÃļschen", "delete_link": "Link lÃļschen", + "delete_local_action_prompt": "{count} lokal gelÃļscht", "delete_local_dialog_ok_backed_up_only": "Nur gesicherte Inhalte lÃļschen", "delete_local_dialog_ok_force": "Trotzdem lÃļschen", "delete_others": "Andere lÃļschen", @@ -744,6 +770,7 @@ "description": "Beschreibung", "description_input_hint_text": "Beschreibung hinzufÃŧgen...", "description_input_submit_error": "Beschreibung konnte nicht geändert werden, bitte im Log fÃŧr mehr Details nachsehen", + "deselect_all": "Alle abwählen", "details": "Details", "direction": "Richtung", "disabled": "Deaktiviert", @@ -761,6 +788,7 @@ "documentation": "Dokumentation", "done": "Fertig", "download": "Herunterladen", + "download_action_prompt": "Herunterladen von {count} Dateien", "download_canceled": "Download abgebrochen", "download_complete": "Download vollständig", "download_enqueue": "Download in die Warteschlange gesetzt", @@ -798,6 +826,7 @@ "edit_key": "SchlÃŧssel bearbeiten", "edit_link": "Link bearbeiten", "edit_location": "Standort bearbeiten", + "edit_location_action_prompt": "{count} Geolokationen angepasst", "edit_location_dialog_title": "Ort bearbeiten", "edit_name": "Name bearbeiten", "edit_people": "Personen bearbeiten", @@ -816,6 +845,7 @@ "empty_trash": "Papierkorb leeren", "empty_trash_confirmation": "Bist du sicher, dass du den Papierkorb leeren willst?\nDies entfernt alle Dateien im Papierkorb endgÃŧltig aus Immich und kann nicht rÃŧckgängig gemacht werden!", "enable": "Aktivieren", + "enable_backup": "Sicherung aktivieren", "enable_biometric_auth_description": "Gib deinen PIN Code ein, um die biometrische Authentifizierung zu aktivieren", "enabled": "Aktiviert", "end_date": "Enddatum", @@ -983,6 +1013,7 @@ "failed_to_load_assets": "Laden der Assets fehlgeschlagen", "failed_to_load_folder": "Fehler beim Laden des Ordners", "favorite": "Favorit", + "favorite_action_prompt": "{count} zu den Favoriten hinzugefÃŧgt", "favorite_or_unfavorite_photo": "Favorisiertes oder nicht favorisiertes Foto", "favorites": "Favoriten", "favorites_page_no_favorites": "Keine favorisierten Inhalte gefunden", @@ -1022,6 +1053,9 @@ "haptic_feedback_switch": "Haptisches Feedback aktivieren", "haptic_feedback_title": "Haptisches Feedback", "has_quota": "Kontingent", + "hash_asset": "Dateihash", + "hashed_assets": "Gehashte Dateien", + "hashing": "Hashen", "header_settings_add_header_tip": "Header hinzufÃŧgen", "header_settings_field_validator_msg": "Der Wert darf nicht leer sein", "header_settings_header_name_input": "Header-Name", @@ -1054,6 +1088,7 @@ "host": "Host", "hour": "Stunde", "id": "ID", + "idle": "Untätig", "ignore_icloud_photos": "iCloud Fotos ignorieren", "ignore_icloud_photos_description": "Fotos, die in der iCloud gespeichert sind, werden nicht auf den immich Server hochgeladen", "image": "Bild", @@ -1126,6 +1161,7 @@ "library_page_sort_created": "Zuletzt erstellt", "library_page_sort_last_modified": "Zuletzt bearbeitet", "library_page_sort_title": "Titel des Albums", + "licenses": "Lizenzen", "light": "Hell", "like_deleted": "Like gelÃļscht", "link_motion_video": "Bewegungsvideo verknÃŧpfen", @@ -1135,7 +1171,9 @@ "list": "Liste", "loading": "Laden", "loading_search_results_failed": "Laden von Suchergebnissen fehlgeschlagen", + "local": "Lokal", "local_asset_cast_failed": "Eine Datei, die nicht auf den Server hochgeladen wurde, kann nicht gecastet werden", + "local_assets": "Lokale Dateien", "local_network": "Lokales Netzwerk", "local_network_sheet_info": "Die App stellt Ãŧber diese URL eine Verbindung zum Server her, wenn sie das angegebene WLAN-Netzwerk verwendet", "location_permission": "Standort Genehmigung", @@ -1245,6 +1283,7 @@ "more": "Mehr", "move": "Verschieben", "move_off_locked_folder": "Aus dem gesperrten Ordner verschieben", + "move_to_lock_folder_action_prompt": "{count} zum gesperrten Ordner hinzugefÃŧgt", "move_to_locked_folder": "In den gesperrten Ordner verschieben", "move_to_locked_folder_confirmation": "Diese Fotos und Videos werden aus allen Alben entfernt und kÃļnnen nur noch im gesperrten Ordner angezeigt werden", "moved_to_archive": "{count, plural, one {# Datei} other {# Dateien}} archiviert", @@ -1291,6 +1330,7 @@ "no_results": "Keine Ergebnisse", "no_results_description": "Versuche es mit einem Synonym oder einem allgemeineren Stichwort", "no_shared_albums_message": "Erstelle ein Album, um Fotos und Videos mit Personen in deinem Netzwerk zu teilen", + "no_uploads_in_progress": "Kein Upload in Bearbeitung", "not_in_any_album": "In keinem Album", "not_selected": "Nicht ausgewählt", "note_apply_storage_label_to_previously_uploaded assets": "Hinweis: Um eine Speicherpfadbezeichnung anzuwenden, starte den", @@ -1328,6 +1368,7 @@ "original": "Original", "other": "Sonstiges", "other_devices": "Andere Geräte", + "other_entities": "Andere Entitäten", "other_variables": "Sonstige Variablen", "owned": "Eigenes", "owner": "Besitzer", @@ -1459,6 +1500,7 @@ "purchase_server_description_2": "UnterstÃŧtzerstatus", "purchase_server_title": "Server", "purchase_settings_server_activated": "Der Server-ProduktschlÃŧssel wird durch den Administrator verwaltet", + "queue_status": "Warteschlange {count}/{total}", "rating": "Bewertung", "rating_clear": "Bewertung lÃļschen", "rating_count": "{count, plural, one {# Stern} other {# Sterne}}", @@ -1487,6 +1529,8 @@ "refreshing_faces": "Gesichter werden aktualisiert", "refreshing_metadata": "Metadaten werden aktualisiert", "regenerating_thumbnails": "Miniaturansichten werden neu erstellt", + "remote": "Entfernt", + "remote_assets": "Entfernte Dateien", "remove": "Entfernen", "remove_assets_album_confirmation": "Bist du sicher, dass du {count, plural, one {# Datei} other {# Dateien}} aus dem Album entfernen willst?", "remove_assets_shared_link_confirmation": "Bist du sicher, dass du {count, plural, one {# Datei} other {# Dateien}} von diesem geteilten Link entfernen willst?", @@ -1494,7 +1538,9 @@ "remove_custom_date_range": "Benutzerdefinierten Datumsbereich entfernen", "remove_deleted_assets": "Offline-Dateien entfernen", "remove_from_album": "Aus Album entfernen", + "remove_from_album_action_prompt": "{count} vom Album entfernt", "remove_from_favorites": "Aus Favoriten entfernen", + "remove_from_lock_folder_action_prompt": "{count} aus dem gesperrten Ordner entfernt", "remove_from_locked_folder": "Aus gesperrtem Ordner entfernen", "remove_from_locked_folder_confirmation": "Bist du sicher, dass du diese Fotos und Videos aus dem gesperrten Ordner entfernen mÃļchtest? Sie werden wieder in deiner Bibliothek sichtbar sein.", "remove_from_shared_link": "Aus geteiltem Link entfernen", @@ -1522,11 +1568,15 @@ "reset_password": "Passwort zurÃŧcksetzen", "reset_people_visibility": "Sichtbarkeit von Personen zurÃŧcksetzen", "reset_pin_code": "PIN Code zurÃŧcksetzen", + "reset_sqlite": "SQLite Datenbank zurÃŧcksetzen", + "reset_sqlite_confirmation": "Bist du sicher, dass du die SQLite-Datenbank zurÃŧcksetzen willst? Du musst dich ab- und wieder anmelden, um die Daten neu zu synchronisieren", + "reset_sqlite_success": "SQLite Datenbank erfolgreich zurÃŧckgesetzt", "reset_to_default": "Auf Standard zurÃŧcksetzen", "resolve_duplicates": "Duplikate entfernen", "resolved_all_duplicates": "Alle Duplikate aufgelÃļst", "restore": "Wiederherstellen", "restore_all": "Alle wiederherstellen", + "restore_trash_action_prompt": "{count} aus dem Papierkorb wiederhergestellt", "restore_user": "Nutzer wiederherstellen", "restored_asset": "Datei wiederhergestellt", "resume": "Fortsetzen", @@ -1535,6 +1585,7 @@ "role": "Rolle", "role_editor": "Bearbeiter", "role_viewer": "Betrachter", + "running": "Läuft", "save": "Speichern", "save_to_gallery": "In Galerie speichern", "saved_api_key": "API-SchlÃŧssel wurde gespeichert", @@ -1666,6 +1717,7 @@ "settings_saved": "Einstellungen gespeichert", "setup_pin_code": "Einen PIN Code festlegen", "share": "Teilen", + "share_action_prompt": "{count} Dateien geteilt", "share_add_photos": "Fotos hinzufÃŧgen", "share_assets_selected": "{count} ausgewählt", "share_dialog_preparing": "Vorbereiten...", @@ -1767,6 +1819,7 @@ "sort_title": "Titel", "source": "Quellcode", "stack": "Stapel", + "stack_action_prompt": "{count} gestapelt", "stack_duplicates": "Duplikate stapeln", "stack_select_one_photo": "Hauptfoto fÃŧr den Stapel auswählen", "stack_selected_photos": "Ausgewählte Fotos stapeln", @@ -1786,6 +1839,7 @@ "storage_quota": "Speicherplatz-Kontingent", "storage_usage": "{used} von {available} verwendet", "submit": "Bestätigen", + "success": "Erfolgreich", "suggestions": "Vorschläge", "sunrise_on_the_beach": "Sonnenaufgang am Strand", "support": "UnterstÃŧtzung", @@ -1795,6 +1849,8 @@ "sync": "Synchronisieren", "sync_albums": "Alben synchronisieren", "sync_albums_manual_subtitle": "Synchronisiere alle hochgeladenen Videos und Fotos in die ausgewählten Backup-Alben", + "sync_local": "Lokal synchronisieren", + "sync_remote": "Entfernt synchronisieren", "sync_upload_album_setting_subtitle": "Erstelle deine ausgewählten Alben in Immich und lade die Fotos und Videos dort hoch", "tag": "Tag", "tag_assets": "Dateien taggen", @@ -1805,6 +1861,7 @@ "tag_updated": "Tag aktualisiert: {tag}", "tagged_assets": "{count, plural, one {# Datei} other {# Dateien}} getagged", "tags": "Tags", + "tap_to_run_job": "Tippen um den Job zu starten", "template": "Vorlage", "theme": "Theme", "theme_selection": "Themenauswahl", @@ -1837,6 +1894,7 @@ "total": "Gesamt", "total_usage": "Gesamtnutzung", "trash": "Papierkorb", + "trash_action_prompt": "{count} in den Papierkorb verschoben", "trash_all": "Alle lÃļschen", "trash_count": "Papierkorb {count, number}", "trash_delete_asset": "Datei lÃļschen/in den Papierkorb verschieben", @@ -1854,9 +1912,11 @@ "unable_to_change_pin_code": "PIN Code konnte nicht geändert werden", "unable_to_setup_pin_code": "PIN Code konnte nicht festgelegt werden", "unarchive": "Entarchivieren", + "unarchive_action_prompt": "{count} aus dem Archiv entfernt", "unarchived_count": "{count, plural, other {# entarchiviert}}", "undo": "RÃŧckgängig", "unfavorite": "Entfavorisieren", + "unfavorite_action_prompt": "{count} aus den Favoriten entfernt", "unhide_person": "Person einblenden", "unknown": "Unbekannt", "unknown_country": "Unbekanntes Land", @@ -1874,12 +1934,15 @@ "unselect_all_duplicates": "Alle Duplikate abwählen", "unselect_all_in": "Alle in {group} abwählen", "unstack": "Entstapeln", + "unstack_action_prompt": "{count} entstapelt", "unstacked_assets_count": "{count, plural, one {# Datei} other {# Dateien}} entstapelt", + "untagged": "Ohne Tag", "up_next": "Weiter", "updated_at": "Aktualisiert", "updated_password": "Passwort aktualisiert", "upload": "Hochladen", "upload_concurrency": "Parallelität beim Hochladen", + "upload_details": "Upload Details", "upload_dialog_info": "Willst du die ausgewählten Elemente auf dem Server sichern?", "upload_dialog_title": "Element hochladen", "upload_errors": "Hochladen mit {count, plural, one {# Fehler} other {# Fehlern}} abgeschlossen, aktualisiere die Seite, um neu hochgeladene Dateien zu sehen.", @@ -1911,6 +1974,7 @@ "user_usage_stats_description": "Statistiken zur Kontonutzung anzeigen", "username": "Nutzername", "users": "Benutzer", + "users_added_to_album_count": "{count, plural, one {# Benutzer} other {# Benutzer}} zum Album hinzugefÃŧgt", "utilities": "Hilfsmittel", "validate": "Validieren", "validate_endpoint_error": "Bitte gib eine gÃŧltige URL ein", @@ -1929,6 +1993,7 @@ "view_album": "Album anzeigen", "view_all": "Alles anzeigen", "view_all_users": "Alle Nutzer anzeigen", + "view_details": "Details ansehen", "view_in_timeline": "In Zeitleiste anzeigen", "view_link": "Link anzeigen", "view_links": "Links anzeigen", diff --git a/i18n/el.json b/i18n/el.json index 62d0481ec6..015bf80c0a 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -464,7 +464,6 @@ "assets": "ΑÎŊĪ„ÎšÎēÎĩίÎŧÎĩÎŊÎą", "assets_added_count": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎĩ {count, plural, one {# ÎąĪĪ‡ÎĩίÎŋ} other {# ÎąĪĪ‡ÎĩÎ¯Îą}}", "assets_added_to_album_count": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎĩ {count, plural, one {# ÎąĪĪ‡ÎĩίÎŋ} other {# ÎąĪĪ‡ÎĩÎ¯Îą}} ĪƒĪ„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", - "assets_added_to_name_count": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎĩ {count, plural, one {# ÎąĪĪ‡ÎĩίÎŋ} other {# ÎąĪĪ‡ÎĩÎ¯Îą}} ĪƒĪ„Îŋ {hasName, select, true {{name}} other {ÎŊέÎŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {ÎŖĪ„ÎŋÎšĪ‡ÎĩίÎŋ} other {ÎŖĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą}} δÎĩÎŊ ÎŧĪ€Îŋ΁ÎŋĪÎŊ ÎŊÎą ΀΁ÎŋĪƒĪ„ÎĩθÎŋĪÎŊ ĪƒĪ„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "assets_count": "{count, plural, one {# ÎąĪĪ‡ÎĩίÎŋ} other {# ÎąĪĪ‡ÎĩÎ¯Îą}}", "assets_deleted_permanently": "{count} Ī„Îą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Î´ÎšÎąÎŗĪÎŦĪ†ÎˇÎēÎąÎŊ ÎŋĪÎšĪƒĪ„ÎšÎēÎŦ", @@ -703,7 +702,6 @@ "daily_title_text_date": "Ε, MMM dd", "daily_title_text_date_year": "Ε, MMM dd, yyyy", "dark": "ÎŖÎēÎŋĪĪÎŋ", - "darkTheme": "ΕÎŊÎąÎģÎģÎąÎŗÎŽ ΃ÎēÎŋĪĪÎŋĪ… θέÎŧÎąĪ„ÎŋĪ‚", "date_after": "ΗÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą ÎŧÎĩĪ„ÎŦ", "date_and_time": "ΗÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą ÎēιΚ ĪŽĪÎą", "date_before": "ΗÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą Ī€ĪÎšÎŊ", diff --git a/i18n/en.json b/i18n/en.json index fafa137f4d..cfc8ffccee 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -166,6 +166,20 @@ "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": "Run facial recognition on newly detected faces", + "nightly_tasks_cluster_new_faces_setting": "Cluster new faces", + "nightly_tasks_database_cleanup_setting": "Database cleanup tasks", + "nightly_tasks_database_cleanup_setting_description": "Clean up old, expired data from the database", + "nightly_tasks_generate_memories_setting": "Generate memories", + "nightly_tasks_generate_memories_setting_description": "Create new memories from assets", + "nightly_tasks_missing_thumbnails_setting": "Generate missing thumbnails", + "nightly_tasks_missing_thumbnails_setting_description": "Queue assets without thumbnails for thumbnail generation", + "nightly_tasks_settings": "Nightly Tasks Settings", + "nightly_tasks_settings_description": "Manage nightly tasks", + "nightly_tasks_start_time_setting": "Start time", + "nightly_tasks_start_time_setting_description": "The time at which the server starts running the nightly tasks", + "nightly_tasks_sync_quota_usage_setting": "Sync quota usage", + "nightly_tasks_sync_quota_usage_setting_description": "Update user storage quota, based on current usage", "no_paths_added": "No paths added", "no_pattern_added": "No pattern added", "note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the", @@ -196,6 +210,8 @@ "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_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", "oauth_settings_description": "Manage OAuth login settings", "oauth_settings_more_details": "For more details about this feature, refer to the docs.", @@ -357,10 +373,12 @@ "admin_password": "Admin Password", "administration": "Administration", "advanced": "Advanced", + "advanced_settings_beta_timeline_subtitle": "Try the new app experience", + "advanced_settings_beta_timeline_title": "Beta Timeline", "advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter", "advanced_settings_log_level_title": "Log level: {level}", - "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", + "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from local assets. Activate this setting to load remote images instead.", "advanced_settings_prefer_remote_title": "Prefer remote images", "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", "advanced_settings_proxy_headers_title": "Proxy Headers", @@ -379,6 +397,7 @@ "album_cover_updated": "Album cover updated", "album_delete_confirmation": "Are you sure you want to delete the album {album}?", "album_delete_confirmation_description": "If this album is shared, other users will not be able to access it anymore.", + "album_deleted": "Album deleted", "album_info_card_backup_album_excluded": "EXCLUDED", "album_info_card_backup_album_included": "INCLUDED", "album_info_updated": "Album info updated", @@ -388,6 +407,7 @@ "album_options": "Album options", "album_remove_user": "Remove user?", "album_remove_user_confirmation": "Are you sure you want to remove {user}?", + "album_search_not_found": "No albums found matching your search", "album_share_no_users": "Looks like you have shared this album with all users or you don't have any user to share with.", "album_updated": "Album updated", "album_updated_setting_description": "Receive an email notification when a shared album has new assets", @@ -407,6 +427,7 @@ "albums_default_sort_order": "Default album sort order", "albums_default_sort_order_description": "Initial asset sort order when creating new albums.", "albums_feature_description": "Collections of assets that can be shared with other users.", + "albums_on_device_count": "Albums on device ({count})", "all": "All", "all_albums": "All albums", "all_people": "All people", @@ -427,6 +448,7 @@ "app_settings": "App Settings", "appears_in": "Appears in", "archive": "Archive", + "archive_action_prompt": "{count} added to Archive", "archive_or_unarchive_photo": "Archive or unarchive photo", "archive_page_no_archived_assets": "No archived assets found", "archive_page_title": "Archive ({count})", @@ -464,7 +486,6 @@ "assets": "Assets", "assets_added_count": "Added {count, plural, one {# asset} other {# assets}}", "assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album", - "assets_added_to_name_count": "Added {count, plural, one {# asset} other {# assets}} to {hasName, select, true {{name}} other {new album}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album", "assets_count": "{count, plural, one {# asset} other {# assets}}", "assets_deleted_permanently": "{count} asset(s) deleted permanently", @@ -490,6 +511,7 @@ "back_close_deselect": "Back, close, or deselect", "background_location_permission": "Background location permission", "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", + "backup": "Backup", "backup_album_selection_page_albums_device": "Albums on device ({count})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", @@ -553,6 +575,8 @@ "backup_options_page_title": "Backup options", "backup_setting_subtitle": "Manage background and foreground upload settings", "backward": "Backward", + "beta_sync": "Beta Sync Status", + "beta_sync_subtitle": "Manage the new sync system", "biometric_auth_enabled": "Biometric authentication enabled", "biometric_locked_out": "You are locked out of biometric authentication", "biometric_no_options": "No biometric options available", @@ -570,7 +594,7 @@ "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", "cache_settings_duplicated_assets_clear_button": "CLEAR", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", + "cache_settings_duplicated_assets_subtitle": "Photos and videos that are ignore listed by the app", "cache_settings_duplicated_assets_title": "Duplicated Assets ({count})", "cache_settings_statistics_album": "Library thumbnails", "cache_settings_statistics_full": "Full images", @@ -587,6 +611,7 @@ "cancel": "Cancel", "cancel_search": "Cancel search", "canceled": "Canceled", + "canceling": "Canceling", "cannot_merge_people": "Cannot merge people", "cannot_undo_this_action": "You cannot undo this action!", "cannot_update_the_description": "Cannot update the description", @@ -703,7 +728,7 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Dark", - "darkTheme": "Toggle dark theme", + "dark_theme": "Toggle dark theme", "date_after": "Date after", "date_and_time": "Date and Time", "date_before": "Date before", @@ -719,6 +744,8 @@ "default_locale": "Default Locale", "default_locale_description": "Format dates and numbers based on your browser locale", "delete": "Delete", + "delete_action_confirmation_message": "Are you sure you want to delete this asset? This action will move the asset to the server's trash and will prompt if you want to delete it locally", + "delete_action_prompt": "{count} deleted", "delete_album": "Delete album", "delete_api_key_prompt": "Are you sure you want to delete this API key?", "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device", @@ -732,9 +759,12 @@ "delete_key": "Delete key", "delete_library": "Delete Library", "delete_link": "Delete link", + "delete_local_action_prompt": "{count} deleted locally", "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", "delete_local_dialog_ok_force": "Delete Anyway", "delete_others": "Delete others", + "delete_permanently": "Delete permanently", + "delete_permanently_action_prompt": "{count} deleted permanently", "delete_shared_link": "Delete shared link", "delete_shared_link_dialog_title": "Delete Shared Link", "delete_tag": "Delete tag", @@ -745,6 +775,7 @@ "description": "Description", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", + "deselect_all": "Deselect All", "details": "Details", "direction": "Direction", "disabled": "Disabled", @@ -762,6 +793,7 @@ "documentation": "Documentation", "done": "Done", "download": "Download", + "download_action_prompt": "Downloading {count} assets", "download_canceled": "Download canceled", "download_complete": "Download complete", "download_enqueue": "Download enqueued", @@ -799,6 +831,7 @@ "edit_key": "Edit key", "edit_link": "Edit link", "edit_location": "Edit location", + "edit_location_action_prompt": "{count} location edited", "edit_location_dialog_title": "Location", "edit_name": "Edit name", "edit_people": "Edit people", @@ -817,6 +850,7 @@ "empty_trash": "Empty trash", "empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!", "enable": "Enable", + "enable_backup": "Enable Backup", "enable_biometric_auth_description": "Enter your PIN code to enable biometric authentication", "enabled": "Enabled", "end_date": "End date", @@ -973,6 +1007,8 @@ "explorer": "Explorer", "export": "Export", "export_as_json": "Export as JSON", + "export_database": "Export Database", + "export_database_description": "Export the SQLite database", "extension": "Extension", "external": "External", "external_libraries": "External Libraries", @@ -984,6 +1020,7 @@ "failed_to_load_assets": "Failed to load assets", "failed_to_load_folder": "Failed to load folder", "favorite": "Favorite", + "favorite_action_prompt": "{count} added to Favorites", "favorite_or_unfavorite_photo": "Favorite or unfavorite photo", "favorites": "Favorites", "favorites_page_no_favorites": "No favorite assets found", @@ -1023,6 +1060,9 @@ "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "has_quota": "Has quota", + "hash_asset": "Hash asset", + "hashed_assets": "Hashed assets", + "hashing": "Hashing", "header_settings_add_header_tip": "Add Header", "header_settings_field_validator_msg": "Value cannot be empty", "header_settings_header_name_input": "Header name", @@ -1055,6 +1095,7 @@ "host": "Host", "hour": "Hour", "id": "ID", + "idle": "Idle", "ignore_icloud_photos": "Ignore iCloud photos", "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", "image": "Image", @@ -1127,6 +1168,7 @@ "library_page_sort_created": "Created date", "library_page_sort_last_modified": "Last modified", "library_page_sort_title": "Album title", + "licenses": "Licenses", "light": "Light", "like_deleted": "Like deleted", "link_motion_video": "Link motion video", @@ -1136,7 +1178,9 @@ "list": "List", "loading": "Loading", "loading_search_results_failed": "Loading search results failed", + "local": "Local", "local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server", + "local_assets": "Local Assets", "local_network": "Local network", "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", "location_permission": "Location permission", @@ -1193,8 +1237,7 @@ "manage_your_devices": "Manage your logged-in devices", "manage_your_oauth_connection": "Manage your OAuth connection", "map": "Map", - "map_assets_in_bound": "{count} photo", - "map_assets_in_bounds": "{count} photos", + "map_assets_in_bounds": "{count, plural, one {# photo} other {# photos}}", "map_cannot_get_user_location": "Cannot get user's location", "map_location_dialog_yes": "Yes", "map_location_picker_page_use_location": "Use this location", @@ -1246,6 +1289,7 @@ "more": "More", "move": "Move", "move_off_locked_folder": "Move out of locked folder", + "move_to_lock_folder_action_prompt": "{count} added to the locked folder", "move_to_locked_folder": "Move to locked folder", "move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder", "moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive", @@ -1292,6 +1336,7 @@ "no_results": "No results", "no_results_description": "Try a synonym or more general keyword", "no_shared_albums_message": "Create an album to share photos and videos with people in your network", + "no_uploads_in_progress": "No uploads in progress", "not_in_any_album": "Not in any album", "not_selected": "Not selected", "note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the", @@ -1329,6 +1374,7 @@ "original": "original", "other": "Other", "other_devices": "Other devices", + "other_entities": "Other entities", "other_variables": "Other variables", "owned": "Owned", "owner": "Owner", @@ -1460,6 +1506,7 @@ "purchase_server_description_2": "Supporter status", "purchase_server_title": "Server", "purchase_settings_server_activated": "The server product key is managed by the admin", + "queue_status": "Queuing {count}/{total}", "rating": "Star rating", "rating_clear": "Clear rating", "rating_count": "{count, plural, one {# star} other {# stars}}", @@ -1488,6 +1535,8 @@ "refreshing_faces": "Refreshing faces", "refreshing_metadata": "Refreshing metadata", "regenerating_thumbnails": "Regenerating thumbnails", + "remote": "Remote", + "remote_assets": "Remote Assets", "remove": "Remove", "remove_assets_album_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from the album?", "remove_assets_shared_link_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from this shared link?", @@ -1495,7 +1544,9 @@ "remove_custom_date_range": "Remove custom date range", "remove_deleted_assets": "Remove Deleted Assets", "remove_from_album": "Remove from album", + "remove_from_album_action_prompt": "{count} removed from the album", "remove_from_favorites": "Remove from favorites", + "remove_from_lock_folder_action_prompt": "{count} removed from the locked folder", "remove_from_locked_folder": "Remove from locked folder", "remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of the locked folder? They will be visible in your library.", "remove_from_shared_link": "Remove from shared link", @@ -1523,11 +1574,15 @@ "reset_password": "Reset password", "reset_people_visibility": "Reset people visibility", "reset_pin_code": "Reset PIN code", + "reset_sqlite": "Reset SQLite Database", + "reset_sqlite_confirmation": "Are you sure you want to reset the SQLite database? You will need to log out and log in again to resync the data", + "reset_sqlite_success": "Successfully reset the SQLite database", "reset_to_default": "Reset to default", "resolve_duplicates": "Resolve duplicates", "resolved_all_duplicates": "Resolved all duplicates", "restore": "Restore", "restore_all": "Restore all", + "restore_trash_action_prompt": "{count} restored from trash", "restore_user": "Restore user", "restored_asset": "Restored asset", "resume": "Resume", @@ -1536,6 +1591,7 @@ "role": "Role", "role_editor": "Editor", "role_viewer": "Viewer", + "running": "Running", "save": "Save", "save_to_gallery": "Save to gallery", "saved_api_key": "Saved API Key", @@ -1667,6 +1723,7 @@ "settings_saved": "Settings saved", "setup_pin_code": "Setup a PIN code", "share": "Share", + "share_action_prompt": "Shared {count} assets", "share_add_photos": "Add photos", "share_assets_selected": "{count} selected", "share_dialog_preparing": "Preparing...", @@ -1768,6 +1825,7 @@ "sort_title": "Title", "source": "Source", "stack": "Stack", + "stack_action_prompt": "{count} stacked", "stack_duplicates": "Stack duplicates", "stack_select_one_photo": "Select one main photo for the stack", "stack_selected_photos": "Stack selected photos", @@ -1787,6 +1845,7 @@ "storage_quota": "Storage Quota", "storage_usage": "{used} of {available} used", "submit": "Submit", + "success": "Success", "suggestions": "Suggestions", "sunrise_on_the_beach": "Sunrise on the beach", "support": "Support", @@ -1796,6 +1855,8 @@ "sync": "Sync", "sync_albums": "Sync albums", "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", + "sync_local": "Sync Local", + "sync_remote": "Sync Remote", "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", "tag": "Tag", "tag_assets": "Tag assets", @@ -1806,6 +1867,7 @@ "tag_updated": "Updated tag: {tag}", "tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}", "tags": "Tags", + "tap_to_run_job": "Tap to run job", "template": "Template", "theme": "Theme", "theme_selection": "Theme selection", @@ -1838,6 +1900,7 @@ "total": "Total", "total_usage": "Total usage", "trash": "Trash", + "trash_action_prompt": "{count} moved to trash", "trash_all": "Trash All", "trash_count": "Trash {count, number}", "trash_delete_asset": "Trash/Delete Asset", @@ -1855,9 +1918,11 @@ "unable_to_change_pin_code": "Unable to change PIN code", "unable_to_setup_pin_code": "Unable to setup PIN code", "unarchive": "Unarchive", + "unarchive_action_prompt": "{count} removed from Archive", "unarchived_count": "{count, plural, other {Unarchived #}}", "undo": "Undo", "unfavorite": "Unfavorite", + "unfavorite_action_prompt": "{count} removed from Favorites", "unhide_person": "Unhide person", "unknown": "Unknown", "unknown_country": "Unknown Country", @@ -1875,15 +1940,20 @@ "unselect_all_duplicates": "Unselect all duplicates", "unselect_all_in": "Unselect all in {group}", "unstack": "Un-stack", + "unstack_action_prompt": "{count} unstacked", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", + "untagged": "Untagged", "up_next": "Up next", "updated_at": "Updated", "updated_password": "Updated password", "upload": "Upload", + "upload_action_prompt": "{count} queued for upload", "upload_concurrency": "Upload concurrency", + "upload_details": "Upload Details", "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", "upload_dialog_title": "Upload Asset", "upload_errors": "Upload completed with {count, plural, one {# error} other {# errors}}, refresh the page to see new upload assets.", + "upload_finished": "Upload finished", "upload_progress": "Remaining {remaining, number} - Processed {processed, number}/{total, number}", "upload_skipped_duplicates": "Skipped {count, plural, one {# duplicate asset} other {# duplicate assets}}", "upload_status_duplicates": "Duplicates", @@ -1892,6 +1962,7 @@ "upload_success": "Upload success, refresh the page to see new upload assets.", "upload_to_immich": "Upload to Immich ({count})", "uploading": "Uploading", + "uploading_media": "Uploading media", "url": "URL", "usage": "Usage", "use_biometric": "Use biometric", @@ -1912,6 +1983,7 @@ "user_usage_stats_description": "View account usage statistics", "username": "Username", "users": "Users", + "users_added_to_album_count": "Added {count, plural, one {# user} other {# users}} to the album", "utilities": "Utilities", "validate": "Validate", "validate_endpoint_error": "Please enter a valid URL", @@ -1930,6 +2002,7 @@ "view_album": "View Album", "view_all": "View All", "view_all_users": "View all users", + "view_details": "View Details", "view_in_timeline": "View in timeline", "view_link": "View link", "view_links": "View links", diff --git a/i18n/es.json b/i18n/es.json index 7bfdc678df..83d1c9d355 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -2,7 +2,7 @@ "about": "Acerca de", "account": "Cuenta", "account_settings": "Ajustes de la cuenta", - "acknowledge": "De acuerdo", + "acknowledge": "Aceptar", "action": "AcciÃŗn", "action_common_update": "Actualizar", "actions": "Acciones", @@ -14,13 +14,13 @@ "add_a_location": "Agregar ubicaciÃŗn", "add_a_name": "Agregar nombre", "add_a_title": "Agregar título", - "add_endpoint": "AÃąadir endpoint", + "add_endpoint": "Agregar endpoint", "add_exclusion_pattern": "Agregar patrÃŗn de exclusiÃŗn", "add_import_path": "Agregar ruta de importaciÃŗn", "add_location": "Agregar ubicaciÃŗn", "add_more_users": "Agregar mÃĄs usuarios", "add_partner": "Agregar compaÃąero", - "add_path": "Agregar carpeta", + "add_path": "Agregar ruta", "add_photos": "Agregar fotos", "add_tag": "Agregar etiqueta", "add_to": "Agregar aâ€Ļ", @@ -63,8 +63,8 @@ "exclusion_pattern_description": "Los patrones de exclusiÃŗn te permiten ignorar archivos y carpetas al escanear tu biblioteca. Es Ãētil si tienes carpetas que contienen archivos que no deseas importar, por ejemplo archivos RAW.", "external_library_management": "GestiÃŗn de bibliotecas externas", "face_detection": "DetecciÃŗn de caras", - "face_detection_description": "Detecta las caras en los activos mediante aprendizaje automÃĄtico. En el caso de los vídeos, solo se tiene en cuenta la miniatura. \"Actualizar\" (re)procesarÃĄ todos los elementos. \"Restablecer\" borra ademÃĄs todos los datos de caras actuales. \"Falta\" pone en cola los elementos que aÃēn no se han procesado. Las caras detectadas se pondrÃĄn en cola para el reconocimiento facial una vez finalizada la detecciÃŗn, agrupÃĄndolos en personas existentes o nuevas.", - "facial_recognition_job_description": "Agrupa las caras detectadas en personas. Este paso se ejecuta una vez finalizada la detecciÃŗn de caras. \"Restablecer\" (re)agrupa todas las caras. \"Falta\" pone en cola las caras que no tienen asignada una persona.", + "face_detection_description": "Detecta las caras en los elementos mediante aprendizaje automÃĄtico. En el caso de los vídeos, solo se tiene en cuenta la miniatura. \"Actualizar\" (re)procesarÃĄ todos los elementos. \"Restablecer\" borra ademÃĄs todos los datos de caras actuales. \"Faltante\" pone en cola los elementos que aÃēn no se han procesado. Las caras detectadas se pondrÃĄn en cola para el reconocimiento facial una vez finalizada la detecciÃŗn, agrupÃĄndolos en personas existentes o nuevas.", + "facial_recognition_job_description": "Agrupa las caras detectadas en personas. Este paso se realiza despuÊs de completar la detecciÃŗn de caras. \"Restablecer\" (re)agrupa todas las caras. \"Faltante\" pone en cola las caras que no tienen una persona asignada.", "failed_job_command": "El comando {command} ha fallado para la tarea: {job}", "force_delete_user_warning": "CUIDADO: Esta acciÃŗn eliminarÃĄ inmediatamente el usuario y todos los elementos. Esta accion no se puede deshacer y los archivos no pueden ser recuperados.", "image_format": "Formato", @@ -166,6 +166,20 @@ "metadata_settings_description": "Administrar la configuraciÃŗn de metadatos", "migration_job": "MigraciÃŗn", "migration_job_description": "Migrar miniaturas de archivos y caras a la estructura de carpetas mÃĄs reciente", + "nightly_tasks_cluster_faces_setting_description": "Ejecutar reconocimiento facial en caras detectadas recientemente", + "nightly_tasks_cluster_new_faces_setting": "Agrupar caras nuevas", + "nightly_tasks_database_cleanup_setting": "Tareas de limpieza de base de datos", + "nightly_tasks_database_cleanup_setting_description": "Limpiar datos antiguos y caducados de la base de datos", + "nightly_tasks_generate_memories_setting": "Generar recuerdos", + "nightly_tasks_generate_memories_setting_description": "Crear nuevos recuerdos a partir de activos", + "nightly_tasks_missing_thumbnails_setting": "Generar miniaturas faltantes", + "nightly_tasks_missing_thumbnails_setting_description": "Poner en cola a activos sin miniaturas para la generaciÃŗn de miniaturas", + "nightly_tasks_settings": "ConfiguraciÃŗn de Tareas Nocturnas", + "nightly_tasks_settings_description": "Gestionar Tareas Nocturnas", + "nightly_tasks_start_time_setting": "Tiempo de inicio", + "nightly_tasks_start_time_setting_description": "El tiempo cuando el servidor comienza a ejecutar las tareas nocturnas", + "nightly_tasks_sync_quota_usage_setting": "Uso de la cuota de sincronizaciÃŗn", + "nightly_tasks_sync_quota_usage_setting_description": "Actualizar la cuota de almacenamiento del usuario, segÃēn el uso actual", "no_paths_added": "No se han aÃąadido carpetas", "no_pattern_added": "No se han aÃąadido patrones", "note_apply_storage_label_previous_assets": "Nota: para aplicar una Etiqueta de Almacenamiento a un elemento anteriormente cargado, lanza el", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "URI de redireccionamiento mÃŗvil", "oauth_mobile_redirect_uri_override": "Sobreescribir URI de redirecciÃŗn mÃŗvil", "oauth_mobile_redirect_uri_override_description": "Habilitar cuando el proveedor de OAuth no permite una URI mÃŗvil, como ''{callback}''", + "oauth_role_claim": "ConcesiÃŗn de rol", + "oauth_role_claim_description": "Otorgar acceso de administrador automÃĄticamente segÃēn la presencia de esta concesiÃŗn. La concesiÃŗn puede tener \"usuario\" o \"admin\".", "oauth_settings": "OAuth", "oauth_settings_description": "Administrar la configuraciÃŗn de inicio de sesiÃŗn de OAuth", "oauth_settings_more_details": "Para mÃĄs detalles acerca de esta característica, consulte la documentaciÃŗn.", @@ -205,7 +221,7 @@ "oauth_storage_quota_claim_description": "Establezca automÃĄticamente la cuota de almacenamiento del usuario al valor de esta solicitud.", "oauth_storage_quota_default": "Cuota de almacenamiento predeterminada (GiB)", "oauth_storage_quota_default_description": "Cuota en GiB que se utilizarÃĄ cuando no se proporcione ninguna por defecto.", - "oauth_timeout": "ExpiraciÃŗn de solicitud", + "oauth_timeout": "Límite de tiempo para la solicitud", "oauth_timeout_description": "Tiempo de espera de solicitudes en milisegundos", "password_enable_description": "Iniciar sesiÃŗn con correo electrÃŗnico y contraseÃąa", "password_settings": "ContraseÃąa de Acceso", @@ -357,10 +373,12 @@ "admin_password": "ContraseÃąa del Administrador", "administration": "AdministraciÃŗn", "advanced": "Avanzada", + "advanced_settings_beta_timeline_subtitle": "Prueba la nueva experiencia de la aplicaciÃŗn", + "advanced_settings_beta_timeline_title": "Cronología beta", "advanced_settings_enable_alternate_media_filter_subtitle": "Usa esta opciÃŗn para filtrar medios durante la sincronizaciÃŗn segÃēn criterios alternativos. Intenta esto solo si tienes problemas con que la aplicaciÃŗn detecte todos los ÃĄlbumes.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Usar filtro alternativo de sincronizaciÃŗn de ÃĄlbumes del dispositivo", "advanced_settings_log_level_title": "Nivel de registro: {level}", - "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de los elementos encontrados en el dispositivo. Activa esta opciÃŗn para cargar imÃĄgenes remotas en su lugar.", + "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas desde los archivos locales. Activa esta opciÃŗn para cargar imÃĄgenes remotas en su lugar.", "advanced_settings_prefer_remote_title": "Preferir imÃĄgenes remotas", "advanced_settings_proxy_headers_subtitle": "Configura headers HTTP que Immich incluirÃĄ en cada peticiÃŗn de red", "advanced_settings_proxy_headers_title": "Cabeceras Proxy", @@ -405,8 +423,8 @@ "albums": "Álbumes", "albums_count": "{count, plural, one {{count, number} Álbum} other {{count, number} Álbumes}}", "albums_default_sort_order": "OrdenaciÃŗn por defecto de los ÃĄlbumes", - "albums_default_sort_order_description": "Orden de clasificaciÃŗn inicial de los activos al crear nuevos ÃĄlbumes.", - "albums_feature_description": "Colecciones de activos que pueden compartirse con otros usuarios.", + "albums_default_sort_order_description": "Orden de clasificaciÃŗn inicial de los recursos al crear nuevos ÃĄlbumes.", + "albums_feature_description": "Colecciones de recursos que pueden ser compartidos con otros usuarios.", "all": "Todos", "all_albums": "Todos los albums", "all_people": "Todas las personas", @@ -427,6 +445,7 @@ "app_settings": "Ajustes de Aplicacion", "appears_in": "Aparece en", "archive": "Archivo", + "archive_action_prompt": "{count} aÃąadidos al Archivo", "archive_or_unarchive_photo": "Archivar o restaurar foto", "archive_page_no_archived_assets": "No se encontraron elementos archivados", "archive_page_title": "Archivo ({count})", @@ -464,13 +483,12 @@ "assets": "elementos", "assets_added_count": "AÃąadido {count, plural, one {# asset} other {# assets}}", "assets_added_to_album_count": "AÃąadido {count, plural, one {# asset} other {# assets}} al ÃĄlbum", - "assets_added_to_name_count": "AÃąadido {count, plural, one {# asset} other {# assets}} a {hasName, select, true {{name}} other {new album}}", - "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} no pueden ser aÃąadidos al album", + "assets_cannot_be_added_to_album_count": "{count, plural, one {El recurso no puede ser aÃąadido al ÃĄlbum} other {Los recursos no pueden ser aÃąadidos al ÃĄlbum}}", "assets_count": "{count, plural, one {# activo} other {# activos}}", "assets_deleted_permanently": "{count} elemento(s) eliminado(s) permanentemente", "assets_deleted_permanently_from_server": "{count} recurso(s) eliminado(s) de forma permanente del servidor de Immich", - "assets_downloaded_failed": "{count, plural, one {Descargado archivo # - {error} archivo fallido} other {Descargados # archivos - {error} archivos fallidos}}", - "assets_downloaded_successfully": "{count, plural, one {Archivo # descargado correctamente} other {Archivos # descargados correctamente}}", + "assets_downloaded_failed": "{count, plural, one {# archivo descargado - {error} archivo fallido} other {# archivos descargados - {error} archivos fallidos}}", + "assets_downloaded_successfully": "{count, plural, one {# archivo descargado exitosamente} other {# archivos descargados exitosamente}}", "assets_moved_to_trash_count": "{count, plural, one {# elemento movido} other {# elementos movidos}} a la papelera", "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}", "assets_removed_count": "Eliminado {count, plural, one {# elemento} other {# elementos}}", @@ -703,7 +721,7 @@ "daily_title_text_date": "E dd, MMM", "daily_title_text_date_year": "E dd de MMM, yyyy", "dark": "Oscuro", - "darkTheme": "Activar tema oscuro", + "dark_theme": "Alternar tema oscuro", "date_after": "Fecha posterior", "date_and_time": "Fecha y Hora", "date_before": "Fecha anterior", @@ -719,6 +737,7 @@ "default_locale": "ConfiguraciÃŗn regional predeterminada", "default_locale_description": "Formatee fechas y nÃēmeros segÃēn la configuraciÃŗn regional de su navegador", "delete": "Eliminar", + "delete_action_prompt": "{count} eliminados permanentemente", "delete_album": "Eliminar ÃĄlbum", "delete_api_key_prompt": "ÂŋEstÃĄ seguro de que desea eliminar esta clave API?", "delete_dialog_alert": "Estos elementos serÃĄn eliminados permanentemente de Immich y de tu dispositivo", @@ -732,6 +751,7 @@ "delete_key": "Eliminar clave", "delete_library": "Eliminar biblioteca", "delete_link": "Eliminar enlace", + "delete_local_action_prompt": "{count} eliminados localmente", "delete_local_dialog_ok_backed_up_only": "Borrar solo las que tengan copia de seguridad", "delete_local_dialog_ok_force": "Borrar de todos modos", "delete_others": "Eliminar otros", @@ -749,6 +769,7 @@ "direction": "DirecciÃŗn", "disabled": "Deshabilitado", "disallow_edits": "Bloquear ediciÃŗn", + "discord": "Discord", "discover": "Descubrir", "discovered_devices": "Dispositivos descubiertos", "dismiss_all_errors": "Descartar todos los errores", @@ -761,6 +782,7 @@ "documentation": "DocumentaciÃŗn", "done": "Hecho", "download": "Descargar", + "download_action_prompt": "Descargando {count} archivos", "download_canceled": "Descarga cancelada", "download_complete": "Descarga completada", "download_enqueue": "Descarga en cola", @@ -798,6 +820,7 @@ "edit_key": "Editar clave", "edit_link": "Editar enlace", "edit_location": "Editar ubicaciÃŗn", + "edit_location_action_prompt": "{count} ubicaciones actualizadas", "edit_location_dialog_title": "UbicaciÃŗn", "edit_name": "Cambiar nombre", "edit_people": "Editar persona", @@ -983,6 +1006,7 @@ "failed_to_load_assets": "Error al cargar los activos", "failed_to_load_folder": "No se pudo cargar la carpeta", "favorite": "Favorito", + "favorite_action_prompt": "{count} aÃąadidos a Favoritos", "favorite_or_unfavorite_photo": "Foto favorita o no favorita", "favorites": "Favoritos", "favorites_page_no_favorites": "No se encontraron elementos marcados como favoritos", @@ -1126,6 +1150,7 @@ "library_page_sort_created": "Creado mÃĄs recientemente", "library_page_sort_last_modified": "Última modificaciÃŗn", "library_page_sort_title": "Título del ÃĄlbum", + "licenses": "Licencias", "light": "Claro", "like_deleted": "Me gusta eliminado", "link_motion_video": "Enlazar vídeo en movimiento", @@ -1135,7 +1160,7 @@ "list": "Listar", "loading": "Cargando", "loading_search_results_failed": "Error al cargar los resultados de la bÃēsqueda", - "local_asset_cast_failed": "No se puede emitir un activo que no estÃĄ cargado en el servidor", + "local_asset_cast_failed": "No es posible transmitir un recurso que no estÃĄ subido al servidor", "local_network": "Red local", "local_network_sheet_info": "La aplicaciÃŗn se conectarÃĄ al servidor a travÊs de esta URL cuando utilice la red Wi-Fi especificada", "location_permission": "Permiso de ubicaciÃŗn", @@ -1238,13 +1263,14 @@ "merged_people_count": "Fusionada {count, plural, one {# persona} other {# personas}}", "minimize": "Minimizar", "minute": "Minuto", - "missing": "Perdido", + "missing": "Faltante", "model": "Modelo", "month": "Mes", - "monthly_title_text_date_format": "MMMM y", + "monthly_title_text_date_format": "MMMM a", "more": "Mas", "move": "Mover", "move_off_locked_folder": "Mover fuera de la carpeta protegida", + "move_to_lock_folder_action_prompt": "{count} aÃąadidos a la carpeta protegida", "move_to_locked_folder": "Mover a la carpeta protegida", "move_to_locked_folder_confirmation": "Estas fotos y vídeos serÃĄn eliminados de todos los ÃĄlbumes y sÃŗlo podrÃĄn ser vistos desde la carpeta protegida", "moved_to_archive": "Movido(s) {count, plural, one {# recurso} other {# recursos}} a archivo", @@ -1277,7 +1303,7 @@ "no_archived_assets_message": "Archive fotos y videos para ocultarlos de su vista de Fotos", "no_assets_message": "HAZ CLIC PARA SUBIR TU PRIMERA FOTO", "no_assets_to_show": "No hay elementos a mostrar", - "no_cast_devices_found": "Dispositivos de difusiÃŗn no encontrados", + "no_cast_devices_found": "No se encontraron dispositivos de transmisiÃŗn", "no_duplicates_found": "No se encontraron duplicados.", "no_exif_info_available": "No hay informaciÃŗn exif disponible", "no_explore_results_message": "Sube mÃĄs fotos para explorar tu colecciÃŗn.", @@ -1494,7 +1520,9 @@ "remove_custom_date_range": "Eliminar intervalo de fechas personalizado", "remove_deleted_assets": "Eliminar archivos sin conexiÃŗn", "remove_from_album": "Eliminar del ÃĄlbum", + "remove_from_album_action_prompt": "{count} eliminado del ÃĄlbum", "remove_from_favorites": "Quitar de favoritos", + "remove_from_lock_folder_action_prompt": "{count} eliminado de la carpeta protegida", "remove_from_locked_folder": "Eliminar de la carpeta protegida", "remove_from_locked_folder_confirmation": "ÂŋEstÃĄs seguro de que deseas mover estas fotos y vídeos fuera de la carpeta protegida? SerÃĄn visibles en tu biblioteca.", "remove_from_shared_link": "Eliminar desde enlace compartido", @@ -1558,17 +1586,17 @@ "search_city": "Buscar ciudad...", "search_country": "Buscar país...", "search_filter_apply": "Aplicar filtros", - "search_filter_camera_title": "Elige tipo de cÃĄmara", + "search_filter_camera_title": "Elegir tipo de cÃĄmara", "search_filter_date": "Fecha", "search_filter_date_interval": "{start} al {end}", - "search_filter_date_title": "Selecciona un intervalo de fechas", + "search_filter_date_title": "Seleccionar un intervalo de fechas", "search_filter_display_option_not_in_album": "No en ÃĄlbum", "search_filter_display_options": "Opciones de visualizaciÃŗn", "search_filter_filename": "Buscar por nombre de archivo", "search_filter_location": "UbicaciÃŗn", "search_filter_location_title": "Seleccionar una ubicaciÃŗn", "search_filter_media_type": "Tipo de archivo", - "search_filter_media_type_title": "Selecciona el tipo de archivo", + "search_filter_media_type_title": "Seleccionar el tipo de archivo", "search_filter_people_title": "Seleccionar personas", "search_for": "Buscar", "search_for_existing_person": "Buscar persona existente", @@ -1591,23 +1619,23 @@ "search_people": "Buscar personas", "search_places": "Buscar lugar", "search_rating": "Buscar por calificaciÃŗn...", - "search_result_page_new_search_hint": "Nueva Busqueda", + "search_result_page_new_search_hint": "Nueva BÃēsqueda", "search_settings": "Ajustes de la bÃēsqueda", "search_state": "Buscar regiÃŗn/estado...", "search_suggestion_list_smart_search_hint_1": "La bÃēsqueda inteligente estÃĄ habilitada por defecto, para buscar metadatos utiliza esta sintaxis ", "search_suggestion_list_smart_search_hint_2": "m:tu-tÊrmino-de-bÃēsqueda", - "search_tags": "Buscando etiquetas...", + "search_tags": "Buscar etiquetas...", "search_timezone": "Buscar zona horaria...", "search_type": "Tipo de bÃēsqueda", "search_your_photos": "Busca tus fotos", "searching_locales": "Buscando sitios...", "second": "Segundo", "see_all_people": "Ver todas las personas", - "select": "Selecciona", + "select": "Seleccionar", "select_album_cover": "Seleccionar portada del ÃĄlbum", "select_all": "Seleccionar todo", "select_all_duplicates": "Seleccionar todos los duplicados", - "select_all_in": "Selecciona todos en {group}", + "select_all_in": "Seleccionar todos en {group}", "select_avatar_color": "Seleccionar color del avatar", "select_face": "Seleccionar cara", "select_featured_photo": "Seleccionar foto principal", @@ -1638,7 +1666,7 @@ "set_date_of_birth": "Establecer fecha de nacimiento", "set_profile_picture": "Establecer foto de perfil", "set_slideshow_to_fullscreen": "Mostrar diapositivas en pantalla completa", - "set_stack_primary_asset": "Establecer como activo principal", + "set_stack_primary_asset": "Establecer como recurso principal", "setting_image_viewer_help": "El visor de detalles carga primero la miniatura pequeÃąa, luego carga la vista previa de tamaÃąo mediano (si estÃĄ habilitada), finalmente carga la original (si estÃĄ habilitada).", "setting_image_viewer_original_subtitle": "Activar para cargar la imagen en resoluciÃŗn original (ÂĄmuy grande!). Deshabilitar para reducir el consumo de datos (de red y cachÊ).", "setting_image_viewer_original_title": "Cargar imagen original", @@ -1666,6 +1694,7 @@ "settings_saved": "Ajustes guardados", "setup_pin_code": "Establecer un PIN", "share": "Compartir", + "share_action_prompt": "{count} recursos compartidos", "share_add_photos": "Agregar fotos", "share_assets_selected": "{count} seleccionado(s)", "share_dialog_preparing": "Preparando...", @@ -1767,6 +1796,7 @@ "sort_title": "Título", "source": "Origen", "stack": "Apilar", + "stack_action_prompt": "{count} apilados", "stack_duplicates": "Apilar duplicados", "stack_select_one_photo": "Selecciona una imagen principal para la pila", "stack_selected_photos": "Apilar fotos seleccionadas", @@ -1776,7 +1806,7 @@ "start_date": "Fecha de inicio", "state": "Estado", "status": "Estado", - "stop_casting": "Parar difusiÃŗn", + "stop_casting": "Detener transmisiÃŗn", "stop_motion_photo": "Parar foto en movimiento", "stop_photo_sharing": "ÂŋDejar de compartir tus fotos?", "stop_photo_sharing_description": "{partner} ya no podrÃĄ acceder a tus fotos.", @@ -1837,6 +1867,7 @@ "total": "Total", "total_usage": "Uso total", "trash": "Papelera", + "trash_action_prompt": "{count} movidos a la papelera", "trash_all": "Descartar todo", "trash_count": "Descartar {count, number}", "trash_delete_asset": "Borrar/Eliminar archivo", @@ -1854,9 +1885,11 @@ "unable_to_change_pin_code": "No se ha podido cambiar el PIN", "unable_to_setup_pin_code": "No se ha podido establecer el PIN", "unarchive": "Desarchivar", + "unarchive_action_prompt": "{count} eliminados del archivo", "unarchived_count": "{count, plural, one {# No archivado} other {# No archivados}}", "undo": "Deshacer", "unfavorite": "Retirar favorito", + "unfavorite_action_prompt": "{count} eliminados de favoritos", "unhide_person": "Mostrar persona", "unknown": "Desconocido", "unknown_country": "País desconocido", @@ -1874,7 +1907,9 @@ "unselect_all_duplicates": "Deseleccionar todos los duplicados", "unselect_all_in": "Deselecciona todos en {group}", "unstack": "Desapilar", + "unstack_action_prompt": "{count} desapilado(s)", "unstacked_assets_count": "Desapilado(s) {count, plural, one {# elemento} other {# elementos}}", + "untagged": "Sin etiqueta", "up_next": "A continuaciÃŗn", "updated_at": "Actualizado", "updated_password": "ContraseÃąa actualizada", @@ -1911,6 +1946,7 @@ "user_usage_stats_description": "Ver estadísticas de uso de la cuenta", "username": "Nombre de usuario", "users": "Usuarios", + "users_added_to_album_count": "{count, plural, one {# usuario agregado} other {# usuarios agregados}} al ÃĄlbum", "utilities": "Utilidades", "validate": "Validar", "validate_endpoint_error": "Por favor, introduce una URL vÃĄlida", diff --git a/i18n/et.json b/i18n/et.json index a2b17c6138..1d293a3bdf 100644 --- a/i18n/et.json +++ b/i18n/et.json @@ -33,11 +33,11 @@ "added_to_favorites": "Lisatud lemmikutesse", "added_to_favorites_count": "{count, number} pilti lisatud lemmikutesse", "admin": { - "add_exclusion_pattern_description": "Lisa välistamismustreid. Toetatud on metamärgid *, ** ja ?. KÃĩikide kataloogis nimega \"Raw\" olevate failide ignoreerimiseks kasuta \"**/Raw/**\". KÃĩikide .tif failide ignoreerimiseks kasuta \"**/*.tif\". Absouutse tee ignoreerimiseks kasuta \"/path/to/ignore/**\".", + "add_exclusion_pattern_description": "Lisa välistamismustreid. Toetatud on metamärgid *, ** ja ?. KÃĩikide kataloogis nimega \"Raw\" olevate failide ignoreerimiseks kasuta \"**/Raw/**\". KÃĩikide \".tif\" lÃĩpuga failide ignoreerimiseks kasuta \"**/*.tif\". Absouutse tee ignoreerimiseks kasuta \"/tee/mida/ignoreerida/**\".", "admin_user": "Administraator", - "asset_offline_description": "Seda välise kogu Ãŧksust ei leitud kettalt ning see liigutati prÃŧgikasti. Kui faili asukoht kogu siseselt muutus, leiad vastava uue Ãŧksuse oma ajajoonelt. Üksuse taastamiseks veendu, et allpool toodud failitee on Immich'ile kättesaadav ning skaneeri kogu uuesti.", + "asset_offline_description": "Seda välise kogu Ãŧksust ei leitud kettalt ning see liigutati prÃŧgikasti. Kui faili asukoht muutus kogu siseselt, leiad vastava uue Ãŧksuse oma ajajoonelt. Üksuse taastamiseks veendu, et allpool toodud failitee on Immich'ile kättesaadav ning skaneeri kogu uuesti.", "authentication_settings": "Autentimise seaded", - "authentication_settings_description": "Halda parooli, OAuth ja muid autentimise seadeid", + "authentication_settings_description": "Halda parooli, OAuth'i ja muid autentimise seadeid", "authentication_settings_disable_all": "Kas oled kindel, et soovid kÃĩik sisselogimismeetodid välja lÃŧlitada? Sisselogimine lÃŧlitatakse täielikult välja.", "authentication_settings_reenable": "Et taas lubada, kasuta serveri käsku.", "background_task_job": "Tausttegumid", @@ -47,26 +47,26 @@ "backup_settings": "Andmebaasi tÃĩmmiste seaded", "backup_settings_description": "Halda andmebaasi tÃĩmmiste seadeid.", "cleared_jobs": "TÃļÃļted eemaldatud: {job}", - "config_set_by_file": "Konfiguratsioon on määratud konfifaili abil", + "config_set_by_file": "Konfiguratsioon on määratud konfiguratsioonifaili abil", "confirm_delete_library": "Kas oled kindel, et soovid kustutada {library} kogu?", - "confirm_delete_library_assets": "Kas oled kindel, et soovid selle kogu kustutada? Sellega kustutatakse {count, plural, one {# sisalduv Ãŧksus} other {kÃĩik # sisalduvat Ãŧksust}} Immich'ist ning seda ei saa tagasi vÃĩtta. Failid jäävad kettale alles.", + "confirm_delete_library_assets": "Kas oled kindel, et soovid selle kogu kustutada? Sellega kustutatakse {count, plural, one {# sisalduv Ãŧksus} other {kÃĩik # sisalduvat Ãŧksust}} Immich'ist ning seda toimingut ei saa tagasi vÃĩtta. Failid jäävad kettale alles.", "confirm_email_below": "Kinnitamiseks sisesta allpool \"{email}\"", "confirm_reprocess_all_faces": "Kas oled kindel, et soovid kÃĩik näod uuesti tÃļÃļdelda? See eemaldab kÃĩik nimega isikud.", "confirm_user_password_reset": "Kas oled kindel, et soovid kasutaja {user} parooli lähtestada?", "confirm_user_pin_code_reset": "Kas oled kindel, et soovid kasutaja {user} PIN-koodi lähtestada?", "create_job": "Lisa tÃļÃļde", "cron_expression": "Cron avaldis", - "cron_expression_description": "Sea skaneerimise intervall cron formaadis. Rohkema info jaoks vaata nt. Crontab Guru", + "cron_expression_description": "Määra skaneerimise intervall cron formaadis. Rohkema info jaoks vaata nt. Crontab Guru", "cron_expression_presets": "Eelseadistatud cron avaldised", "disable_login": "Keela sisselogimine", "duplicate_detection_job_description": "Rakenda Ãŧksustele masinÃĩpet, et leida sarnaseid pilte. Kasutab nutiotsingut", - "exclusion_pattern_description": "Välistamismustrid vÃĩimaldavad ignoreerida faile ja kaustu kogu skaneerimisel. See on kasulik, kui sul on kaustu, mis sisaldavad faile, mida sa ei soovi importida, nagu RAW failid.", + "exclusion_pattern_description": "Välistamismustrid vÃĩimaldavad ignoreerida faile ja kaustu selle kogu skaneerimisel. See on kasulik, kui sul on kaustu, mis sisaldavad faile, mida sa ei soovi importida, nagu RAW failid.", "external_library_management": "Väliste kogude haldus", "face_detection": "Näoavastus", - "face_detection_description": "Avasta Ãŧksustest nägusid masinÃĩppe abil. Videote puhul kasutatakse ainult pisipilti. \"Värskenda\" tÃļÃļtleb kÃĩik Ãŧksused uuesti. \"Lähtesta\" kustutab lisaks kÃĩik seni leitud näed. \"Puuduvad\" vÃĩtab ette Ãŧksused, mida pole veel tÃļÃļdeldud. Avastatud näod suunatakse näotuvastusse, et grupeerida nad olemasolevateks vÃĩi uuteks isikuteks.", + "face_detection_description": "Avasta Ãŧksustest nägusid masinÃĩppe abil. Videote puhul kasutatakse ainult pisipilti. \"Värskenda\" tÃļÃļtleb kÃĩik Ãŧksused uuesti. \"Lähtesta\" kustutab lisaks kÃĩik seni leitud näod. \"Puuduvad\" vÃĩtab ette Ãŧksused, mida pole veel tÃļÃļdeldud. Avastatud näod suunatakse näotuvastusse, et grupeerida nad olemasolevateks vÃĩi uuteks isikuteks.", "facial_recognition_job_description": "Grupeeri avastatud näod inimesteks. See samm käivitub siis, kui näoavastus on lÃĩppenud. \"Lähtesta\" grupeerib kÃĩik näod uuesti. \"Puuduvad\" vÃĩtab ette näod, mida pole isikuga seostatud.", "failed_job_command": "Käsk {command} ebaÃĩnnestus tÃļÃļtes: {job}", - "force_delete_user_warning": "HOIATUS: See kustutab koheselt kasutaja ja kÃĩik Ãŧksused. Seda ei saa tagasi vÃĩtta ja faile ei saa taastada.", + "force_delete_user_warning": "HOIATUS: See kustutab koheselt kasutaja ja kÃĩik tema Ãŧksused. Toimingut ei saa tagasi vÃĩtta ja faile ei saa taastada.", "image_format": "Formaat", "image_format_description": "WebP failid on väiksemad kui JPEG, aga kodeerimine on aeglasem.", "image_fullsize_description": "TäismÃĩÃĩdus pilt ilma metaandmeteta, kasutatakse sisse suumimisel", @@ -77,9 +77,9 @@ "image_prefer_embedded_preview": "Eelista manustatud eelvaadet", "image_prefer_embedded_preview_setting_description": "Kasuta pilditÃļÃļtluse sisendina vÃĩimalusel RAW fotodesse manustatud eelvaateid. See vÃĩib mÃĩnede piltide puhul anda tulemuseks täpsemad värvid, aga eelvaate kvaliteet sÃĩltub konkreetsest kaamerast ning pildis vÃĩib olla rohkem tihendusmÃŧra.", "image_prefer_wide_gamut": "Eelista laia värvigammat", - "image_prefer_wide_gamut_setting_description": "Kasuta pisipiltide jaoks Display P3. See säilitab paremini laia värviruumiga piltide erksuse, aga vanematel seadmetel ja vanemate brauseritega vÃĩivad pildid teistsugused välja näha. sRGB pildid säilitatakse värvinihete vältimiseks.", + "image_prefer_wide_gamut_setting_description": "Kasuta pisipiltide jaoks Display P3. See säilitab paremini laia värviruumiga piltide erksuse, kuid vanematel seadmetel ja vanemate brauseritega vÃĩivad pildid teistsugused välja näha. sRGB pildid säilitatakse värvinihete vältimiseks.", "image_preview_description": "Keskmise suurusega pilt ilma metaandmeteta, kasutusel Ãŧksiku Ãŧksuse vaatamise ja masinÃĩppe jaoks", - "image_preview_quality_description": "Eelvaate kvaliteet vahemikus 1-100. KÃĩrgem väärtus on parem, aga tekitab suuremaid faile ning vÃĩib mÃĩjutada rakenduse tÃļÃļkiirust. Madala väärtuse seadmine vÃĩib mÃĩjutada masinÃĩppe kvaliteeti.", + "image_preview_quality_description": "Eelvaate kvaliteet vahemikus 1-100. KÃĩrgem väärtus on parem, aga tekitab suuremaid faile ning vÃĩib mÃĩjutada rakenduse tÃļÃļkiirust. Madal väärtus vÃĩib mÃĩjutada masinÃĩppe kvaliteeti.", "image_preview_title": "Eelvaate seaded", "image_quality": "Kvaliteet", "image_resolution": "Resolutsioon", @@ -92,7 +92,7 @@ "job_concurrency": "{job} samaaegsus", "job_created": "TÃļÃļde lisatud", "job_not_concurrency_safe": "Seda tÃļÃļdet pole ohutu samaaegselt käivitada.", - "job_settings": "TÃļÃļte seaded", + "job_settings": "TÃļÃļdete seaded", "job_settings_description": "Halda tÃļÃļdete samaaegsust", "job_status": "TÃļÃļte seisund", "jobs_delayed": "{jobCount, plural, other {# edasi lÃŧkatud}}", @@ -166,6 +166,20 @@ "metadata_settings_description": "Halda metaandmete seadeid", "migration_job": "Migratsioon", "migration_job_description": "Migreeri Ãŧksuste ja nägude pisipildid uusimale kaustastruktuurile", + "nightly_tasks_cluster_faces_setting_description": "Käivita värskelt avastatud nägudel näotuvastus", + "nightly_tasks_cluster_new_faces_setting": "Grupeeri uued näod", + "nightly_tasks_database_cleanup_setting": "Andmebaasi puhastuse tegumid", + "nightly_tasks_database_cleanup_setting_description": "Eemalda andmebaasist vanad, aegunud andmed", + "nightly_tasks_generate_memories_setting": "Genereeri mälestused", + "nightly_tasks_generate_memories_setting_description": "Loo Ãŧksustest uued mälestused", + "nightly_tasks_missing_thumbnails_setting": "Genereeri puuduvad pisipildid", + "nightly_tasks_missing_thumbnails_setting_description": "Suuna ilma pisipiltideta Ãŧksused pisipiltide genereerimisele", + "nightly_tasks_settings": "Öiste tegumite seaded", + "nightly_tasks_settings_description": "Halda Ãļiseid tegumeid", + "nightly_tasks_start_time_setting": "Algusaeg", + "nightly_tasks_start_time_setting_description": "Aeg, millal server alustab Ãļiste tegumite käivitamist", + "nightly_tasks_sync_quota_usage_setting": "SÃŧnkrooni kvoodikasutus", + "nightly_tasks_sync_quota_usage_setting_description": "Uuenda kasutaja talletuskvoot jooksva kasutuse alusel", "no_paths_added": "Ühtegi teed pole", "no_pattern_added": "Mustreid ei ole", "note_apply_storage_label_previous_assets": "Märkus: Et rakendada talletussilt varem Ãŧleslaaditud Ãŧksustele, käivita", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "Mobiilne Ãŧmbersuunamise URI", "oauth_mobile_redirect_uri_override": "Mobiilse Ãŧmbersuunamise URI Ãŧlekirjutamine", "oauth_mobile_redirect_uri_override_description": "LÃŧlita sisse, kui OAuth pakkuja ei luba mobiilset URI-d, näiteks ''{callback}''", + "oauth_role_claim": "Rolli väide", + "oauth_role_claim_description": "Anna selle väite olemasolul automaatselt administraatori ligipääs. Väite väärtus vÃĩib olla 'user' vÃĩi 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "Halda OAuth sisselogimise seadeid", "oauth_settings_more_details": "Selle funktsiooni kohta rohkem teada saamiseks loe dokumentatsiooni.", @@ -357,10 +373,12 @@ "admin_password": "Administraatori parool", "administration": "Administratsioon", "advanced": "Täpsemad valikud", + "advanced_settings_beta_timeline_subtitle": "Koge uut rakendust", + "advanced_settings_beta_timeline_title": "Beeta ajajoon", "advanced_settings_enable_alternate_media_filter_subtitle": "Kasuta seda valikut, et filtreerida sÃŧnkroonimise ajal Ãŧksuseid alternatiivsete kriteeriumite alusel. Proovi seda ainult siis, kui rakendusel on probleeme kÃĩigi albumite tuvastamisega.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTAALNE] Kasuta alternatiivset seadme albumi sÃŧnkroonimise filtrit", "advanced_settings_log_level_title": "Logimistase: {level}", - "advanced_settings_prefer_remote_subtitle": "MÃĩned seadmed laadivad seadmes olevate Ãŧksuste pisipilte piinavalt aeglaselt. Aktiveeri see seadistus, et laadida selle asemel kaugpilte.", + "advanced_settings_prefer_remote_subtitle": "MÃĩned seadmed laadivad lokaalsete Ãŧksuste pisipilte piinavalt aeglaselt. Aktiveeri see seadistus, et laadida selle asemel kaugpilte.", "advanced_settings_prefer_remote_title": "Eelista kaugpilte", "advanced_settings_proxy_headers_subtitle": "Määra vaheserveri päised, mida Immich peaks iga päringuga saatma", "advanced_settings_proxy_headers_title": "Vaheserveri päised", @@ -388,6 +406,7 @@ "album_options": "Albumi valikud", "album_remove_user": "Eemalda kasutaja?", "album_remove_user_confirmation": "Kas oled kindel, et soovid kasutaja {user} eemaldada?", + "album_search_not_found": "Otsingule vastavaid albumeid ei leitud", "album_share_no_users": "Paistab, et oled seda albumit kÃĩikide kasutajatega jaganud, vÃĩi pole Ãŧhtegi kasutajat, kellega jagada.", "album_updated": "Album muudetud", "album_updated_setting_description": "Saa teavitus e-posti teel, kui jagatud albumis on uusi Ãŧksuseid", @@ -407,6 +426,7 @@ "albums_default_sort_order": "Vaikimisi albumi järjestus", "albums_default_sort_order_description": "Uute albumite lisamisel Ãŧksuste esialgne järjekord.", "albums_feature_description": "Üksuste kollektsioonid, mida saab teiste kasutajatega jagada.", + "albums_on_device_count": "Albumid seadmel ({count})", "all": "KÃĩik", "all_albums": "KÃĩik albumid", "all_people": "KÃĩik isikud", @@ -427,6 +447,7 @@ "app_settings": "Rakenduse seaded", "appears_in": "Albumid", "archive": "Arhiiv", + "archive_action_prompt": "{count} lisatud arhiivi", "archive_or_unarchive_photo": "Arhiveeri vÃĩi taasta foto", "archive_page_no_archived_assets": "Arhiveeritud Ãŧksuseid ei leitud", "archive_page_title": "Arhiveeri ({count})", @@ -464,7 +485,6 @@ "assets": "Üksused", "assets_added_count": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}} lisatud", "assets_added_to_album_count": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}} albumisse lisatud", - "assets_added_to_name_count": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}} lisatud {hasName, select, true {albumisse {name}} other {uude albumisse}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Üksust} other {Üksuseid}} ei saa albumisse lisada", "assets_count": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}}", "assets_deleted_permanently": "{count} Ãŧksus(t) jäädavalt kustutatud", @@ -553,6 +573,8 @@ "backup_options_page_title": "Varundamise valikud", "backup_setting_subtitle": "Halda taustal ja esiplaanil Ãŧleslaadimise seadeid", "backward": "Tagasi", + "beta_sync": "Beeta sÃŧnkroonimise staatus", + "beta_sync_subtitle": "Halda uut sÃŧnkroonimissÃŧsteemi", "biometric_auth_enabled": "Biomeetriline autentimine lubatud", "biometric_locked_out": "Biomeetriline autentimine on blokeeritud", "biometric_no_options": "Biomeetrilisi valikuid ei ole", @@ -570,7 +592,7 @@ "cache_settings_clear_cache_button": "TÃŧhjenda puhver", "cache_settings_clear_cache_button_title": "TÃŧhjendab rakenduse puhvri. See mÃĩjutab oluliselt rakenduse jÃĩudlust, kuni puhver uuesti täidetakse.", "cache_settings_duplicated_assets_clear_button": "TÜHJENDA", - "cache_settings_duplicated_assets_subtitle": "Fotod ja videod, mis on rakenduse poolt mustfiltreeritud", + "cache_settings_duplicated_assets_subtitle": "Fotod ja videod, mis on rakenduse poolt ignoreeritud", "cache_settings_duplicated_assets_title": "Dubleeritud Ãŧksused ({count})", "cache_settings_statistics_album": "Kogu pisipildid", "cache_settings_statistics_full": "TäismÃĩÃĩdus pildid", @@ -587,6 +609,7 @@ "cancel": "Katkesta", "cancel_search": "Katkesta otsing", "canceled": "TÃŧhistatud", + "canceling": "TÃŧhistamine", "cannot_merge_people": "Ei saa isikuid Ãŧhendada", "cannot_undo_this_action": "Sa ei saa seda tagasi vÃĩtta!", "cannot_update_the_description": "Kirjelduse muutmine ebaÃĩnnestus", @@ -703,7 +726,7 @@ "daily_title_text_date": "d. MMMM", "daily_title_text_date_year": "d. MMMM yyyy", "dark": "Tume", - "darkTheme": "LÃŧlita tume teema", + "dark_theme": "LÃŧlita tume teema", "date_after": "Kuupäev pärast", "date_and_time": "Kuupäev ja kellaaeg", "date_before": "Kuupäev enne", @@ -719,6 +742,7 @@ "default_locale": "Vaikimisi lokaat", "default_locale_description": "Vorminda kuupäevad ja numbrid vastavalt brauseri lokaadile", "delete": "Kustuta", + "delete_action_prompt": "{count} jäädavalt kustutatud", "delete_album": "Kustuta album", "delete_api_key_prompt": "Kas oled kindel, et soovid selle API vÃĩtme kustutada?", "delete_dialog_alert": "Need Ãŧksused kustutatakse jäädavalt Immich'ist ja sinu seadmest", @@ -732,6 +756,7 @@ "delete_key": "Kustuta vÃĩti", "delete_library": "Kustuta kogu", "delete_link": "Kustuta link", + "delete_local_action_prompt": "{count} kustutatud lokaalselt", "delete_local_dialog_ok_backed_up_only": "Kustuta ainult varundatud", "delete_local_dialog_ok_force": "Kustuta sellegipoolest", "delete_others": "Kustuta teised", @@ -745,6 +770,7 @@ "description": "Kirjeldus", "description_input_hint_text": "Lisa kirjeldus...", "description_input_submit_error": "Viga kirjelduse muutmisel, rohkem infot leiad logist", + "deselect_all": "Eemalda kÃĩik valikust", "details": "Üksikasjad", "direction": "Suund", "disabled": "Välja lÃŧlitatud", @@ -762,6 +788,7 @@ "documentation": "Dokumentatsioon", "done": "Tehtud", "download": "Laadi alla", + "download_action_prompt": "{count} Ãŧksust laaditakse alla", "download_canceled": "Allalaadimine katkestatud", "download_complete": "Allalaadimine lÃĩpetatud", "download_enqueue": "Allalaadimine ootel", @@ -799,6 +826,7 @@ "edit_key": "Muuda vÃĩtit", "edit_link": "Muuda linki", "edit_location": "Muuda asukohta", + "edit_location_action_prompt": "{count} asukoht muudetud", "edit_location_dialog_title": "Asukoht", "edit_name": "Muuda nime", "edit_people": "Muuda isikuid", @@ -817,6 +845,7 @@ "empty_trash": "TÃŧhjenda prÃŧgikast", "empty_trash_confirmation": "Kas oled kindel, et soovid prÃŧgikasti tÃŧhjendada? See eemaldab kÃĩik seal olevad Ãŧksused Immich'ist jäädavalt.\nSeda tegevust ei saa tagasi vÃĩtta!", "enable": "Luba", + "enable_backup": "Luba varundus", "enable_biometric_auth_description": "Biomeetrilise autentimise lubamiseks sisesta oma PIN-kood", "enabled": "Lubatud", "end_date": "LÃĩppkuupäev", @@ -984,6 +1013,7 @@ "failed_to_load_assets": "Üksuste laadimine ebaÃĩnnestus", "failed_to_load_folder": "Kausta laadimine ebaÃĩnnestus", "favorite": "Lemmik", + "favorite_action_prompt": "{count} lisatud lemmikutesse", "favorite_or_unfavorite_photo": "Lisa foto lemmikutesse vÃĩi eemalda lemmikutest", "favorites": "Lemmikud", "favorites_page_no_favorites": "Lemmikuid Ãŧksuseid ei leitud", @@ -1023,6 +1053,9 @@ "haptic_feedback_switch": "Luba haptiline tagasiside", "haptic_feedback_title": "Haptiline tagasiside", "has_quota": "On kvoot", + "hash_asset": "Arvuta Ãŧksuse räsi", + "hashed_assets": "Räsiga Ãŧksused", + "hashing": "Räsi arvutamine", "header_settings_add_header_tip": "Lisa päis", "header_settings_field_validator_msg": "Väärtus ei saa olla tÃŧhi", "header_settings_header_name_input": "Päise nimi", @@ -1055,6 +1088,7 @@ "host": "Host", "hour": "Tund", "id": "ID", + "idle": "JÃĩude", "ignore_icloud_photos": "Ignoreeri iCloud fotosid", "ignore_icloud_photos_description": "Fotosid, mis on iCloud'is, ei laadita Ãŧles Immich'i serverisse", "image": "Pilt", @@ -1099,7 +1133,7 @@ "ios_debug_info_no_processes_queued": "Taustaprotsesse pole järjekorras", "ios_debug_info_no_sync_yet": "Taustal sÃŧnkroonimise tÃļÃļde pole veel käinud", "ios_debug_info_processes_queued": "{count, plural, one {{count} taustaprotsess järjekorras} other {{count} taustaprotsessi järjekorras}}", - "ios_debug_info_processing_ran_at": "TÃļÃļtlemine käis {dateTime}", + "ios_debug_info_processing_ran_at": "TÃļÃļtlemine toimus {dateTime}", "items_count": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}}", "jobs": "TÃļÃļted", "keep": "Jäta alles", @@ -1127,6 +1161,7 @@ "library_page_sort_created": "Loomise aeg", "library_page_sort_last_modified": "Viimase muutmise aeg", "library_page_sort_title": "Albumi pealkiri", + "licenses": "Litsentsid", "light": "Hele", "like_deleted": "Meeldimine kustutatud", "link_motion_video": "Lingi liikuv video", @@ -1136,7 +1171,9 @@ "list": "Loend", "loading": "Laadimine", "loading_search_results_failed": "Otsitulemuste laadimine ebaÃĩnnestus", + "local": "Lokaalne Ãŧksus", "local_asset_cast_failed": "Ei saa edastada Ãŧksust, mis pole serverisse Ãŧles laaditud", + "local_assets": "Lokaalsed Ãŧksused", "local_network": "Kohalik vÃĩrk", "local_network_sheet_info": "Rakendus Ãŧhendub valitud Wi-Fi vÃĩrgus olles serveriga selle URL-i kaudu", "location_permission": "Asukoha luba", @@ -1246,6 +1283,7 @@ "more": "Rohkem", "move": "Liiguta", "move_off_locked_folder": "Liiguta lukustatud kaustast välja", + "move_to_lock_folder_action_prompt": "{count} lisatud lukustatud kausta", "move_to_locked_folder": "Liiguta lukustatud kausta", "move_to_locked_folder_confirmation": "Need fotod ja videod eemaldatakse kÃĩigist albumitest ning nad on nähtavad ainult lukustatud kaustas", "moved_to_archive": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}} liigutatud arhiivi", @@ -1292,6 +1330,7 @@ "no_results": "Vasteid pole", "no_results_description": "Proovi sÃŧnonÃŧÃŧmi vÃĩi Ãŧldisemat märksÃĩna", "no_shared_albums_message": "Lisa album, et fotosid ja videosid teistega jagada", + "no_uploads_in_progress": "Üleslaadimisi käimas ei ole", "not_in_any_album": "Pole Ãŧheski albumis", "not_selected": "Ei ole valitud", "note_apply_storage_label_to_previously_uploaded assets": "Märkus: Et rakendada talletussilt varem Ãŧleslaaditud Ãŧksustele, käivita", @@ -1329,6 +1368,7 @@ "original": "originaal", "other": "Muud", "other_devices": "Muud seadmed", + "other_entities": "Muud objektid", "other_variables": "Muud muutujad", "owned": "Minu omad", "owner": "Omanik", @@ -1460,6 +1500,7 @@ "purchase_server_description_2": "Toetaja staatus", "purchase_server_title": "Server", "purchase_settings_server_activated": "Serveri tootevÃĩtit haldab administraator", + "queue_status": "Järjekorras {count}/{total}", "rating": "Hinnang", "rating_clear": "TÃŧhjenda hinnang", "rating_count": "{count, plural, one {# tärn} other {# tärni}}", @@ -1488,6 +1529,8 @@ "refreshing_faces": "Nägude värskendamine", "refreshing_metadata": "Metaandmete värskendamine", "regenerating_thumbnails": "Pisipiltide uuesti genereerimine", + "remote": "KaugÃŧksus", + "remote_assets": "KaugÃŧksused", "remove": "Eemalda", "remove_assets_album_confirmation": "Kas oled kindel, et soovid {count, plural, one {# Ãŧksuse} other {# Ãŧksust}} albumist eemaldada?", "remove_assets_shared_link_confirmation": "Kas oled kindel, et soovid eemaldada {count, plural, one {# Ãŧksuse} other {# Ãŧksust}} sellelt jagatud lingilt?", @@ -1495,7 +1538,9 @@ "remove_custom_date_range": "Eemalda kohandatud kuupäevavahemik", "remove_deleted_assets": "Eemalda kustutatud Ãŧksused", "remove_from_album": "Eemalda albumist", + "remove_from_album_action_prompt": "{count} eemaldatud albumist", "remove_from_favorites": "Eemalda lemmikutest", + "remove_from_lock_folder_action_prompt": "{count} eemaldatud lukustatud kaustast", "remove_from_locked_folder": "Eemalda lukustatud kaustast", "remove_from_locked_folder_confirmation": "Kas oled kindel, et soovid need fotod ja videod lukustatud kaustast välja liigutada? Need muutuvad su kogus nähtavaks.", "remove_from_shared_link": "Eemalda jagatud lingist", @@ -1523,11 +1568,15 @@ "reset_password": "Lähtesta parool", "reset_people_visibility": "Lähtesta isikute nähtavus", "reset_pin_code": "Lähtesta PIN-kood", + "reset_sqlite": "Lähtesta SQLite andmebaas", + "reset_sqlite_confirmation": "Kas oled kindel, et soovid SQLite andmebaasi lähtestada? Andmete uuesti sÃŧnkroonimiseks pead välja ja jälle sisse logima", + "reset_sqlite_success": "SQLite andmebaas edukalt lähtestatud", "reset_to_default": "Lähtesta", "resolve_duplicates": "Lahenda duplikaadid", "resolved_all_duplicates": "KÃĩik duplikaadid lahendatud", "restore": "Taasta", "restore_all": "Taasta kÃĩik", + "restore_trash_action_prompt": "{count} prÃŧgikastust taastatud", "restore_user": "Taasta kasutaja", "restored_asset": "Üksus taastatud", "resume": "Jätka", @@ -1536,6 +1585,7 @@ "role": "Roll", "role_editor": "Muutja", "role_viewer": "Vaataja", + "running": "Käimas", "save": "Salvesta", "save_to_gallery": "Salvesta galeriisse", "saved_api_key": "API vÃĩti salvestatud", @@ -1667,6 +1717,7 @@ "settings_saved": "Seaded salvestatud", "setup_pin_code": "Seadista PIN-kood", "share": "Jaga", + "share_action_prompt": "Jagatud {count} Ãŧksust", "share_add_photos": "Lisa fotosid", "share_assets_selected": "{count} valitud", "share_dialog_preparing": "Ettevalmistamine...", @@ -1768,6 +1819,7 @@ "sort_title": "Pealkiri", "source": "Lähtekood", "stack": "Virnasta", + "stack_action_prompt": "{count} virnastatud", "stack_duplicates": "Virnasta duplikaadid", "stack_select_one_photo": "Vali virnale kaanefoto", "stack_selected_photos": "Virnasta valitud fotod", @@ -1787,6 +1839,7 @@ "storage_quota": "Talletuskvoot", "storage_usage": "{used}/{available} kasutatud", "submit": "Saada", + "success": "Õnnestus", "suggestions": "Soovitused", "sunrise_on_the_beach": "PäikesetÃĩus rannal", "support": "Tugi", @@ -1796,6 +1849,8 @@ "sync": "SÃŧnkrooni", "sync_albums": "SÃŧnkrooni albumid", "sync_albums_manual_subtitle": "SÃŧnkrooni kÃĩik Ãŧleslaaditud videod ja fotod valitud varundusalbumitesse", + "sync_local": "SÃŧnkrooni lokaalsed Ãŧksused", + "sync_remote": "SÃŧnkrooni kaugÃŧksused", "sync_upload_album_setting_subtitle": "Loo ja laadi oma pildid ja videod Ãŧles Immich'isse valitud albumitesse", "tag": "Silt", "tag_assets": "Sildista Ãŧksuseid", @@ -1806,6 +1861,7 @@ "tag_updated": "Muudetud silt: {tag}", "tagged_assets": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}} sildistatud", "tags": "Sildid", + "tap_to_run_job": "Puuduta tÃļÃļte käivitamiseks", "template": "Mall", "theme": "Teema", "theme_selection": "Teema valik", @@ -1838,6 +1894,7 @@ "total": "Kokku", "total_usage": "Kogukasutus", "trash": "PrÃŧgikast", + "trash_action_prompt": "{count} liigutatud prÃŧgikasti", "trash_all": "KÃĩik prÃŧgikasti", "trash_count": "Liiguta {count, number} prÃŧgikasti", "trash_delete_asset": "Kustuta Ãŧksus", @@ -1855,9 +1912,11 @@ "unable_to_change_pin_code": "PIN-koodi muutmine ebaÃĩnnestus", "unable_to_setup_pin_code": "PIN-koodi seadistamine ebaÃĩnnestus", "unarchive": "Taasta arhiivist", + "unarchive_action_prompt": "{count} eemaldatud arhiivist", "unarchived_count": "{count, plural, other {# arhiivist taastatud}}", "undo": "VÃĩta tagasi", "unfavorite": "Eemalda lemmikutest", + "unfavorite_action_prompt": "{count} eemaldatud lemmikutest", "unhide_person": "Ära peida isikut", "unknown": "Teadmata", "unknown_country": "Tundmatu riik", @@ -1875,12 +1934,15 @@ "unselect_all_duplicates": "Ära vali duplikaate", "unselect_all_in": "Ära vali Ãŧhtegi grupis {group}", "unstack": "Eralda", + "unstack_action_prompt": "{count} eraldatud", "unstacked_assets_count": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}} eraldatud", + "untagged": "Sildistamata", "up_next": "Järgmine", "updated_at": "Uuendatud", "updated_password": "Parool muudetud", "upload": "Laadi Ãŧles", "upload_concurrency": "Üleslaadimise samaaegsus", + "upload_details": "Üleslaadimise Ãŧksikasjad", "upload_dialog_info": "Kas soovid valitud Ãŧksuse(d) serverisse varundada?", "upload_dialog_title": "Üksuse Ãŧleslaadimine", "upload_errors": "Üleslaadimine lÃĩpetatud {count, plural, one {# veaga} other {# veaga}}, uute Ãŧksuste nägemiseks värskenda lehte.", @@ -1912,6 +1974,7 @@ "user_usage_stats_description": "Vaata konto kasutuse statistikat", "username": "Kasutajanimi", "users": "Kasutajad", + "users_added_to_album_count": "{count, plural, one {# kasutaja} other {# kasutajat}} lisatud albumisse", "utilities": "TÃļÃļriistad", "validate": "Valideeri", "validate_endpoint_error": "Sisesta korrektne URL", @@ -1930,6 +1993,7 @@ "view_album": "Vaata albumit", "view_all": "Vaata kÃĩiki", "view_all_users": "Vaata kÃĩiki kasutajaid", + "view_details": "Vaata Ãŧksikasju", "view_in_timeline": "Vaata ajajoonel", "view_link": "Vaata linki", "view_links": "Vaata linke", diff --git a/i18n/fi.json b/i18n/fi.json index f41d0cf631..4e12253cf2 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -166,6 +166,20 @@ "metadata_settings_description": "Hallitse metatietoja", "migration_job": "Migraatio", "migration_job_description": "Migroi aineiston pikkukuvat ja kasvot uusimpaan kansiorakenteeseen", + "nightly_tasks_cluster_faces_setting_description": "Aja kasvojen tunnistus uusiin tunnistettuihin kasvoihin", + "nightly_tasks_cluster_new_faces_setting": "Kokoa uudet kasvot", + "nightly_tasks_database_cleanup_setting": "Tietokannan puhdistuksen tehtävät", + "nightly_tasks_database_cleanup_setting_description": "Siivoa vanhentunut data tietokannasta", + "nightly_tasks_generate_memories_setting": "Luo muistoja", + "nightly_tasks_generate_memories_setting_description": "Luo kohteista uusia muistoja", + "nightly_tasks_missing_thumbnails_setting": "Luo puuttuvat pikkukuvat", + "nightly_tasks_missing_thumbnails_setting_description": "Laita ilman pikkukuvia olevat kohteet jonoon pikkukuvien luontia varten", + "nightly_tasks_settings": "YÃļllisten tehtävien asetukset", + "nightly_tasks_settings_description": "Hallitse yÃļllisiä tehtäviä", + "nightly_tasks_start_time_setting": "Aloitusaika", + "nightly_tasks_start_time_setting_description": "Aika jolloin palvelin aloittaa yÃļllisten tehtävien ajon", + "nightly_tasks_sync_quota_usage_setting": "SynkronointikiintiÃļn käyttÃļ", + "nightly_tasks_sync_quota_usage_setting_description": "Päivitä käyttäjän tallennustilan kiintiÃļ nykyisen käytÃļn mukaan", "no_paths_added": "Polkuja ei asetettu", "no_pattern_added": "Kaavoja ei lisättynä", "note_apply_storage_label_previous_assets": "Huom: Asettaaksesi nimikkeen aiemmin ladatulle aineistolle, aja", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "Mobiilin uudellenohjaus-URI", "oauth_mobile_redirect_uri_override": "Ohita mobiilin uudelleenohjaus-URI", "oauth_mobile_redirect_uri_override_description": "Ota käyttÃļÃļn kun OAuth tarjoaja ei salli mobiili URI:a, kuten ''{callback}''", + "oauth_role_claim": "Roolin vaatimus", + "oauth_role_claim_description": "Salli pääkäyttäjän pääsyoikeus automaattisesti tämän vaatimuksen perusteella. Vaatimus voi sisältää, joko 'käyttäjän' tai 'pääkäyttäjän'.", "oauth_settings": "OAuth", "oauth_settings_description": "Hallitse OAuth-kirjautumisen asetuksia", "oauth_settings_more_details": "Saadaksesi lisätietoja tästä toiminnosta, katso dokumentaatio.", @@ -244,6 +260,7 @@ "storage_template_migration_info": "Tallennusmalli muuntaa kaikki tiedostopäätteet pieniksi kirjaimiksi. Mallipohjan muutokset koskevat vain uusia resursseja. Jos haluat käyttää mallipohjaa takautuvasti aiemmin ladattuihin resursseihin, suorita {job}.", "storage_template_migration_job": "Tallennustilan mallin muutostyÃļ", "storage_template_more_details": "Saadaksesi lisätietoa tästä ominaisuudesta, katso Tallennustilan Mallit sekä mihin se vaikuttaa", + "storage_template_onboarding_description_v2": "Päälle kytkettynä, toiminto järjestestelee tiedostot automaattisesti käyttäjän määrittämän mallin mukaisesti. Lisätietoja dokumentaatiosta..", "storage_template_path_length": "Arvioitu tiedostopolun pituusrajoitus: {length, number}/{limit, number}", "storage_template_settings": "Tallennustilan malli", "storage_template_settings_description": "Hallitse palvelimelle ladatun aineiston kansiorakennetta ja tiedostonimiä", @@ -403,6 +420,9 @@ "album_with_link_access": "Anna kenen tahansa nähdä linkin kautta tämän albumin valokuvat ja henkilÃļt.", "albums": "Albumit", "albums_count": "{count, plural, one {{count, number} albumi} other {{count, number} albumia}}", + "albums_default_sort_order": "Albumin oletuslajittelujärjestys", + "albums_default_sort_order_description": "Kohteiden ensisijainen lajittelujärjestys uusia albumeja luotaessa.", + "albums_feature_description": "Kokoelma kohteita, jotka voidaan jakaa muille käyttäjille.", "all": "Kaikki", "all_albums": "Kaikki albumit", "all_people": "Kaikki henkilÃļt", @@ -423,6 +443,7 @@ "app_settings": "Sovellusasetukset", "appears_in": "Esiintyy albumeissa", "archive": "Arkisto", + "archive_action_prompt": "{count} lisätty arkistoon", "archive_or_unarchive_photo": "Arkistoi kuva tai palauta arkistosta", "archive_page_no_archived_assets": "Arkistoituja kohteita ei lÃļytynyt", "archive_page_title": "Arkisto ({count})", @@ -460,10 +481,12 @@ "assets": "Kohteet", "assets_added_count": "Lisätty {count, plural, one {# kohde} other {# kohdetta}}", "assets_added_to_album_count": "Albumiin lisätty {count, plural, one {# kohde} other {# kohdetta}}", - "assets_added_to_name_count": "Lisätty {count, plural, one {# kohde} other {# kohdetta}} {hasName, select, true {{name}} other {uuteen albumiin}}", + "assets_cannot_be_added_to_album_count": "{count, plural, one {Kohdetta} other {Kohdetta}} ei voida lisätä albumiin", "assets_count": "{count, plural, one {# media} other {# mediaa}}", "assets_deleted_permanently": "{count} kohdetta poistettu pysyvästi", "assets_deleted_permanently_from_server": "{count} objektia poistettu pysyvästi Immich-palvelimelta", + "assets_downloaded_failed": "{count, plural, one {Ladattu # tiedosto - {error} tiedosto epäonnistui} other {ladattu # tiedostoa - {error} tiedostot epäonnistuivat}}", + "assets_downloaded_successfully": "{count, plural, one {Ladattu # tiedosto onnistuneesti} other {Ladattu # tiedostoa onnistuneesti}}", "assets_moved_to_trash_count": "Siirretty {count, plural, one {# media} other {# mediaa}} roskakoriin", "assets_permanently_deleted_count": "{count, plural, one {# media} other {# mediaa}} poistettu pysyvästi", "assets_removed_count": "{count, plural, one {# media} other {# mediaa}} poistettu", @@ -478,6 +501,7 @@ "authorized_devices": "Valtuutetut laitteet", "automatic_endpoint_switching_subtitle": "Yhdistä paikallisesti nimetyn Wi-Fi-yhteyden kautta, kun se on saatavilla, ja käytä vaihtoehtoisia yhteyksiä muualla", "automatic_endpoint_switching_title": "Automaattinen URL-osoitteen vaihto", + "autoplay_slideshow": "Toista diaesitys automaattisesti", "back": "Takaisin", "back_close_deselect": "Palaa, sulje tai poista valinnat", "background_location_permission": "Taustasijainnin käyttÃļoikeus", @@ -582,7 +606,8 @@ "cannot_merge_people": "Ihmisiä ei voitu yhdistää", "cannot_undo_this_action": "Et voi perua tätä toimintoa!", "cannot_update_the_description": "Kuvausta ei voi päivittää", - "cast": "Lähettää", + "cast": "Suoratoisto", + "cast_description": "Määritä saatavilla olevat suoratoistopalvelut", "change_date": "Vaihda päiväys", "change_description": "Muuta kuvausta", "change_display_order": "Muuta näyttÃļjärjestystä", @@ -641,6 +666,7 @@ "confirm_password": "Vahvista salasana", "confirm_tag_face": "Haluatko merkitä nämä kasvot nimellä {name}?", "confirm_tag_face_unnamed": "MerkitäänkÃļ nämä kasvot?", + "connected_device": "Yhdistetty laite", "connected_to": "Yhdistetty", "contain": "Mahduta", "context": "Konteksti", @@ -693,6 +719,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Tumma", + "dark_theme": "Vaihda tumma teema", "date_after": "Päivämäärän jälkeen", "date_and_time": "Päivämäärä ja aika", "date_before": "Päivä ennen", @@ -708,6 +735,7 @@ "default_locale": "Oletuskieliasetus", "default_locale_description": "Muotoile päivämäärät ja numerot selaimesi kielen mukaan", "delete": "Poista", + "delete_action_prompt": "{count} poistettu pysyvästi", "delete_album": "Poista albumi", "delete_api_key_prompt": "Haluatko varmasti poistaa tämän API-avaimen?", "delete_dialog_alert": "Nämä kohteet poistetaan pysyvästi Immich:stä ja laitteeltasi", @@ -740,12 +768,13 @@ "disallow_edits": "Älä salli muokkauksia", "discord": "Discord", "discover": "Tutki", + "discovered_devices": "LÃļydetyt laitteet", "dismiss_all_errors": "Sivuuta kaikki virheet", "dismiss_error": "Sivuuta virhe", "display_options": "NäyttÃļasetukset", "display_order": "NäyttÃļjärjestys", "display_original_photos": "Näytä alkuperäiset kuvat", - "display_original_photos_setting_description": "Näytä mieluiten alkuperäinen kuva peukalokuvan sijasta kun alkuperäinen aineisto on web-yhteensopiva. Tämä voi aiheuttaa kuvien näyttämisen hitautta.", + "display_original_photos_setting_description": "Näytä mieluiten alkuperäinen kuva esikatselukuvan sijasta, kun alkuperäinen kuva on web-yhteensopiva. Tämä voi aiheuttaa kuvien näyttämisen hitautta.", "do_not_show_again": "Älä näytä tätä enää", "documentation": "Dokumentaatio", "done": "Valmis", @@ -787,6 +816,7 @@ "edit_key": "Muokkaa avainta", "edit_link": "Muokkaa linkkiä", "edit_location": "Muokkaa sijaintia", + "edit_location_action_prompt": "{count} sijaintia muokattu", "edit_location_dialog_title": "Sijainti", "edit_name": "Muokkaa nimeä", "edit_people": "Muokkaa henkilÃļitä", @@ -972,6 +1002,7 @@ "failed_to_load_assets": "Kohteiden lataus epäonnistui", "failed_to_load_folder": "Kansion lataaminen epäonnistui", "favorite": "Suosikki", + "favorite_action_prompt": "{count} lisätty suosikkeihin", "favorite_or_unfavorite_photo": "Suosikki- tai ei-suosikkikuva", "favorites": "Suosikit", "favorites_page_no_favorites": "Suosikkikohteita ei lÃļytynyt", @@ -1086,6 +1117,8 @@ "ios_debug_info_last_sync_at": "Viimeisin synkronisointi {dateTime}", "ios_debug_info_no_processes_queued": "Ei taustaprosesseja jonossa", "ios_debug_info_no_sync_yet": "Taustasynkronisointia ei ole suoritettu vielä", + "ios_debug_info_processes_queued": "{count, plural, one {{count} taustaprosessi jonossa} other {{count} taustaprosessia jonossa}}", + "ios_debug_info_processing_ran_at": "Prosessi valmistui {dateTime}", "items_count": "{count, plural, one {# kpl} other {# kpl}}", "jobs": "Taustatehtävät", "keep": "Säilytä", @@ -1094,6 +1127,9 @@ "kept_this_deleted_others": "Tämä kohde säilytettiin. {count, plural, one {# asset} other {# assets}} poistettiin", "keyboard_shortcuts": "Pikanäppäimet", "language": "Kieli", + "language_no_results_subtitle": "Yritä säätää hakuehtoja", + "language_no_results_title": "Kieliä ei lÃļydetty", + "language_search_hint": "Etsi kieliä...", "language_setting_description": "Valitse suosimasi kieli", "last_seen": "Viimeksi nähty", "latest_version": "Viimeisin versio", @@ -1119,6 +1155,7 @@ "list": "Lista", "loading": "Ladataan", "loading_search_results_failed": "Hakutulosten lataaminen epäonnistui", + "local_asset_cast_failed": "Kohdetta, joka ei ole ladattuna palvelimelle, ei voida striimata", "local_network": "Lähiverkko", "local_network_sheet_info": "Sovellus muodostaa yhteyden palvelimeen tämän URL-osoitteen kautta, kun käytetään määritettyä Wi-Fi-verkkoa", "location_permission": "Sijainnin käyttÃļoikeus", @@ -1132,6 +1169,7 @@ "locked_folder": "Lukittu kansio", "log_out": "Kirjaudu ulos", "log_out_all_devices": "Kirjaudu ulos kaikilta laitteilta", + "logged_in_as": "Kirjautunut käyttäjänä {user}", "logged_out_all_devices": "Kaikki laitteet kirjattu ulos", "logged_out_device": "Laite kirjattu ulos", "login": "Kirjaudu", @@ -1220,13 +1258,14 @@ "merged_people_count": "{count, plural, one {# HenkilÃļ} other {# henkilÃļä}} yhdistetty", "minimize": "PIenennä", "minute": "Minuutti", - "missing": "Puuttuu", + "missing": "Puuttuvat", "model": "Malli", "month": "Kuukauden mukaan", "monthly_title_text_date_format": "MMMM y", "more": "Enemmän", "move": "Siirrä", "move_off_locked_folder": "Siirrä pois lukitusta kansiosta", + "move_to_lock_folder_action_prompt": "{count} lisätty lukittuun kansioon", "move_to_locked_folder": "Siirrä lukittuun kansioon", "move_to_locked_folder_confirmation": "Nämä kuvat ja videot poistetaan kaikista albumeista, ja ne ovat nähtävissä vain lukitussa kansiossa", "moved_to_archive": "Siirretty {count, plural, one {# kohde} other {# kohdetta}} arkistoon", @@ -1259,6 +1298,7 @@ "no_archived_assets_message": "Arkistoi kuvia ja videoita piilottaaksesi ne kuvat näkymästä", "no_assets_message": "NAPAUTA LATAAKSESI ENSIMMÄISEN KUVASI", "no_assets_to_show": "Ei näytettäviä kohteita", + "no_cast_devices_found": "Cast-laitteita ei lÃļytynyt", "no_duplicates_found": "Kaksoiskappaleita ei lÃļytynyt.", "no_exif_info_available": "EXIF-tietoa ei saatavilla", "no_explore_results_message": "Lataa lisää kuvia tutkiaksesi kokoelmaasi.", @@ -1291,8 +1331,11 @@ "oldest_first": "Vanhin ensin", "on_this_device": "Laitteella", "onboarding": "KäyttÃļÃļnotto", - "onboarding_privacy_description": "Seuraavat (valinnaiset) ominaisuudet perustuvat ulkoisiin palveluihin, ja ne voidaan poistaa käytÃļstä milloin tahansa hallinta asetuksista.", + "onboarding_locale_description": "Valitse haluamasi kieli. Voit muuttaa kieliasetuksia myÃļhemmin asetuksista.", + "onboarding_privacy_description": "Seuraavat (valinnaiset) ominaisuudet perustuvat ulkoisiin palveluihin ja ne voidaan milloin tahansa poistaa käytÃļstä asetuksista.", + "onboarding_server_welcome_description": "Määritellään seuraavaksi järjestelmäsi muutamalla yleisellä asetuksella.", "onboarding_theme_description": "Valitse väriteema istunnollesi. Voit muuttaa tämän myÃļhemmin asetuksistasi.", + "onboarding_user_welcome_description": "Aloitetaan!", "onboarding_welcome_user": "Tervetuloa {user}", "online": "Online", "only_favorites": "Vain suosikit", @@ -1392,7 +1435,7 @@ "previous_or_next_photo": "Kuva seuraava/edellinen", "previous_or_next_year": "Vuosi seuraava/edellinen", "primary": "Ensisijainen", - "privacy": "Yksityisyys", + "privacy": "Tietosuoja", "profile": "Profiili", "profile_drawer_app_logs": "Lokit", "profile_drawer_client_out_of_date_major": "Sovelluksen mobiiliversio on vanhentunut. Päivitä viimeisimpään merkittävään versioon.", @@ -1472,12 +1515,15 @@ "remove_custom_date_range": "Poista aikaväliltä", "remove_deleted_assets": "Poista Offline-tiedostot", "remove_from_album": "Poista albumista", + "remove_from_album_action_prompt": "{count} poistettu albumista", "remove_from_favorites": "Poista suosikeista", + "remove_from_lock_folder_action_prompt": "{count} poistettu lukitusta albumista", "remove_from_locked_folder": "Poista lukitusta kansiosta", "remove_from_locked_folder_confirmation": "Haluatko varmasti siirtää nämä kuvat ja videot pois lukitusta kansiosta? Ne näkyvät sen jälkeen kirjastossasi.", "remove_from_shared_link": "Poista jakolinkistä", "remove_memory": "Tyhjennä muisti", "remove_photo_from_memory": "Poista kuva muistista", + "remove_tag": "Poista tunniste", "remove_url": "Poista URL", "remove_user": "Poista käyttäjä", "removed_api_key": "API-avain {name} poistettu", @@ -1584,6 +1630,7 @@ "select_album_cover": "Valitse albmin kansi", "select_all": "Valitse kaikki", "select_all_duplicates": "Valitse kaikki kaksoiskappaleet", + "select_all_in": "Valitse kaikki {group}", "select_avatar_color": "Valitse avatarin väri", "select_face": "Valitse kasvo", "select_featured_photo": "Valitse esittelykuva", @@ -1604,6 +1651,7 @@ "server_info_box_server_url": "Palvelimen URL-osoite", "server_offline": "Palvelin Offline-tilassa", "server_online": "Palvelin Online-tilassa", + "server_privacy": "Palvelimen tietosuoja", "server_stats": "Palvelimen tilastot", "server_version": "Palvelimen versio", "set": "Aseta", @@ -1613,6 +1661,7 @@ "set_date_of_birth": "Aseta syntymäaika", "set_profile_picture": "Aseta profiilikuva", "set_slideshow_to_fullscreen": "Näytä diaesitys koko ruudulla", + "set_stack_primary_asset": "Aseta pääkohteeksi", "setting_image_viewer_help": "Kuvaa katseltaessa ensin ladataan pikkukuva, sitten keskilaatuinen pikkukuva (jos käytÃļssä) ja lopuksi alkuperäinen (jos käytÃļssä).", "setting_image_viewer_original_subtitle": "Ota käyttÃļÃļn ladataksesi alkuperäinen täysitarkkuuksinen kuva (suuri!). Poista käytÃļstä vähentääksesi datan käyttÃļä (sekä verkossa että laitteen välimuistissa).", "setting_image_viewer_original_title": "Lataa alkuperäinen kuva", @@ -1750,6 +1799,7 @@ "start_date": "Alkupäivä", "state": "Maakunta", "status": "Tila", + "stop_casting": "Lopeta suoratoisto", "stop_motion_photo": "Pysäytä liikkuva kuva", "stop_photo_sharing": "Lopetetaanko kuvien jakaminen?", "stop_photo_sharing_description": "{partner} ei enää pääse kuviisi.", @@ -1769,7 +1819,7 @@ "sync_albums": "Synkronoi albumit", "sync_albums_manual_subtitle": "Synkronoi kaikki ladatut videot ja valokuvat valittuihin varmuuskopioalbumeihin", "sync_upload_album_setting_subtitle": "Luo ja lataa valokuvasi ja videosi valittuihin albumeihin Immichissä", - "tag": "Lisää tunniste", + "tag": "Tunniste", "tag_assets": "Lisää tunnisteita", "tag_created": "Luotu tunniste: {tag}", "tag_feature_description": "Selaa valokuvia ja videoita, jotka on ryhmitelty loogisten tunnisteotsikoiden mukaan", @@ -1810,6 +1860,7 @@ "total": "Yhteensä", "total_usage": "KäyttÃļ yhteensä", "trash": "Roskakori", + "trash_action_prompt": "{count} siirretty roskakoriin", "trash_all": "Vie kaikki roskakoriin", "trash_count": "Roskakori {count, number}", "trash_delete_asset": "Poista / vie roskakoriin", @@ -1827,8 +1878,11 @@ "unable_to_change_pin_code": "PIN-koodin vaihtaminen epäonnistui", "unable_to_setup_pin_code": "PIN-koodin määrittäminen epäonnistui", "unarchive": "Palauta arkistosta", + "unarchive_action_prompt": "{count} poistettu arkistosta", "unarchived_count": "{count, plural, other {# poistettu arkistosta}}", + "undo": "Kumoa", "unfavorite": "Poista suosikeista", + "unfavorite_action_prompt": "{count} poistettu suosikeista", "unhide_person": "Poista henkilÃļ piilosta", "unknown": "Tuntematon", "unknown_country": "Tuntematon maa", @@ -1844,8 +1898,10 @@ "unsaved_change": "Tallentamaton muutos", "unselect_all": "Poista valinnat", "unselect_all_duplicates": "Poista kaikkien kaksoiskappaleiden valinta", + "unselect_all_in": "Poista kaikki valinnat {group}", "unstack": "Pura pino", "unstacked_assets_count": "Poistettu pinosta {count, plural, one {# kohde} other {# kohdetta}}", + "untagged": "Ilman tunnistetta", "up_next": "Seuraavaksi", "updated_at": "Päivitetty", "updated_password": "Salasana päivitetty", @@ -1861,7 +1917,7 @@ "upload_status_uploaded": "Ladattu", "upload_success": "Lataus onnistui. Päivitä sivu jotta näet latauksesi.", "upload_to_immich": "Lähetä Immichiin ({count})", - "uploading": "Lähettään", + "uploading": "Lähettää", "url": "URL", "usage": "KäyttÃļ", "use_biometric": "Käytä biometriikkaa", @@ -1873,6 +1929,7 @@ "user_liked": "{user} tykkäsi {type, select, photo {kuvasta} video {videosta} asset {mediasta} other {tästä}}", "user_pin_code_settings": "PIN-koodi", "user_pin_code_settings_description": "Hallinnoi PIN-koodiasi", + "user_privacy": "Käyttäjän tietosuoja", "user_purchase_settings": "Osta", "user_purchase_settings_description": "Hallitse ostostasi", "user_role_set": "Tee käyttäjästä {user} {role}", diff --git a/i18n/fil.json b/i18n/fil.json index 12e74f7bad..12e6086064 100644 --- a/i18n/fil.json +++ b/i18n/fil.json @@ -14,10 +14,13 @@ "add_a_location": "Dagdagan ng lugar", "add_a_name": "Dagdagan ng pangalan", "add_a_title": "Dagdagan ng pamagat", + "add_endpoint": "Dagdagan ng dulo", "add_location": "Magdagdag ng lugar", "add_more_users": "Magdagdag ng mga user", "add_partner": "Magdagdag ng kasangga", + "add_path": "Magdagdag ng path", "add_photos": "Magdagdag ng litrato", + "add_tag": "Magdagdag ng tag", "add_to": "Idagdag saâ€Ļ", "add_to_album": "Idagdag sa album", "add_to_album_bottom_sheet_added": "Naidagdag sa {album}", diff --git a/i18n/fr.json b/i18n/fr.json index bf57469944..ff9f783771 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -17,7 +17,7 @@ "add_endpoint": "Ajouter une adresse", "add_exclusion_pattern": "Ajouter un schÊma d'exclusion", "add_import_path": "Ajouter un chemin à importer", - "add_location": "Ajouter un lieu", + "add_location": "Ajouter une localisation", "add_more_users": "Ajouter plus d'utilisateurs", "add_partner": "Ajouter un partenaire", "add_path": "Ajouter un chemin", @@ -166,6 +166,20 @@ "metadata_settings_description": "Gestion des paramètres de mÊtadonnÊes", "migration_job": "Migration", "migration_job_description": "Migration des miniatures pour les mÊdias et les visages vers la dernière structure de dossiers", + "nightly_tasks_cluster_faces_setting_description": "DÊmarrer la reconnaissance faciale sur les visages nouvellement dÊtectÊs", + "nightly_tasks_cluster_new_faces_setting": "Regrouper les nouveaux visages", + "nightly_tasks_database_cleanup_setting": "TÃĸches de nettoyage de la base de donnÊes", + "nightly_tasks_database_cleanup_setting_description": "Nettoyage ancien, donnÊes de la base de donnÊes expirÊes", + "nightly_tasks_generate_memories_setting": "GÊnÊrer les souvenirs", + "nightly_tasks_generate_memories_setting_description": "CrÊer des souvenirs à partir des ÊlÊments", + "nightly_tasks_missing_thumbnails_setting": "GÊnÊrer les miniatures manquantes", + "nightly_tasks_missing_thumbnails_setting_description": "Mettre en file d'attente les ÊlÊments sans miniature pour la crÊation de miniature", + "nightly_tasks_settings": "Paramètres des tÃĸches de nuit", + "nightly_tasks_settings_description": "GÊrer les tÃĸches de nuit", + "nightly_tasks_start_time_setting": "Heure de dÊmarrage", + "nightly_tasks_start_time_setting_description": "Heure à laquelle le serveur commence à exÊcuter les tÃĸches de nuit", + "nightly_tasks_sync_quota_usage_setting": "Synchroniser les quota d'usage", + "nightly_tasks_sync_quota_usage_setting_description": "Mettre à jour les quota d'usage de l'utilisateur, en se basant sur l'utilisation actuelle", "no_paths_added": "Aucun chemin n'a ÊtÊ ajoutÊ", "no_pattern_added": "Aucun schÊma d'exclusion n'a ÊtÊ ajoutÊ", "note_apply_storage_label_previous_assets": "Remarque : pour appliquer l'Êtiquette de stockage à des mÊdias prÊcÊdemment envoyÊs, exÊcutez", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "URI de redirection mobile", "oauth_mobile_redirect_uri_override": "Remplacer l'URI de redirection mobile", "oauth_mobile_redirect_uri_override_description": "Activer quand le fournisseur d'OAuth ne permet pas un URI mobile, comme ''{callback}''", + "oauth_role_claim": "Attribut de rôle", + "oauth_role_claim_description": "Donne automatiquement un accès en tant qu'admin, en se basant sur la prÊsence de cet attribut. L'attribut peut avoir soit 'user' (utilisateur) soit 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "GÊrer les paramètres de connexion OAuth", "oauth_settings_more_details": "Pour plus de dÊtails sur cette fonctionnalitÊ, consultez ce lien.", @@ -244,7 +260,7 @@ "storage_template_migration_info": "L'enregistrement des modèles va convertir toutes les extensions en minuscule. Les changements de modèle ne s'appliqueront qu'aux nouveaux mÊdias. Pour appliquer rÊtroactivement le modèle aux mÊdias prÊcÊdemment envoyÊs, exÊcutez la tÃĸche {job}.", "storage_template_migration_job": "TÃĸche de migration du modèle de stockage", "storage_template_more_details": "Pour plus de dÊtails sur cette fonctionnalitÊ, reportez-vous au Modèle de stockage et à ses implications", - "storage_template_onboarding_description_v2": "Quand elle est activÊe, cette fonctionnalitÊ organise automatiquement les fichiers, sur base d'un modèle dÊfini par l'utilisateur. Pour plus d'informations, se rÊpÊter à la documentation.", + "storage_template_onboarding_description_v2": "Quand elle est activÊe, cette fonctionnalitÊ organise automatiquement les fichiers, sur base d'un modèle dÊfini par l'utilisateur. Pour plus d'informations, se rÊfÊrer à la documentation.", "storage_template_path_length": "Limite approximative de la longueur du chemin : {length, number}/{limit, number}", "storage_template_settings": "Modèle de stockage", "storage_template_settings_description": "GÊrer la structure des dossiers et le nom des fichiers du mÊdia envoyÊ", @@ -357,10 +373,12 @@ "admin_password": "Mot de passe Admin", "administration": "Administration", "advanced": "AvancÊ", + "advanced_settings_beta_timeline_subtitle": "Essayer la nouvelle application", + "advanced_settings_beta_timeline_title": "Timeline de la bÊta", "advanced_settings_enable_alternate_media_filter_subtitle": "Utilisez cette option pour filtrer les mÊdia durant la synchronisation avec des critères alternatifs. N'utilisez cela que lorsque l'application n'arrive pas à dÊtecter tout les albums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPÉRIMENTAL] Utiliser le filtre de synchronisation d'album alternatif", "advanced_settings_log_level_title": "Niveau de journalisation : {level}", - "advanced_settings_prefer_remote_subtitle": "Certains appareils sont très lents à charger des miniatures à partir de ressources prÊsentes sur l'appareil. Activez ce paramètre pour charger des images externes à la place.", + "advanced_settings_prefer_remote_subtitle": "Certains appareils sont très lents à charger des miniatures à partir de ressources locales. Activez ce paramètre pour charger des images externes à la place.", "advanced_settings_prefer_remote_title": "PrÊfÊrer les images externes", "advanced_settings_proxy_headers_subtitle": "Ajoutez des en-tÃĒtes personnalisÊs à chaque requÃĒte rÊseau", "advanced_settings_proxy_headers_title": "En-tÃĒtes de proxy", @@ -388,6 +406,7 @@ "album_options": "Options de l'album", "album_remove_user": "Supprimer l'utilisateur ?", "album_remove_user_confirmation": "Êtes-vous sÃģr de vouloir supprimer {user} ?", + "album_search_not_found": "Aucun album trouvÊ ne correspond à votre recherche", "album_share_no_users": "Il semble que vous ayez partagÊ cet album avec tous les utilisateurs ou que vous n'ayez aucun utilisateur avec lequel le partager.", "album_updated": "Album mis à jour", "album_updated_setting_description": "Recevoir une notification par courriel lorsqu'un album partagÊ a de nouveaux mÊdias", @@ -407,14 +426,15 @@ "albums_default_sort_order": "Ordre de tri par dÊfaut des albums", "albums_default_sort_order_description": "Ordre de tri des mÊdias pour les nouveaux albums crÊÊs.", "albums_feature_description": "Bibliothèques de mÊdias pouvant ÃĒtre partagÊs avec d'autres utilisateurs.", + "albums_on_device_count": "Album sur l'appareil ({count})", "all": "Tout", "all_albums": "Tous les albums", "all_people": "Toutes les personnes", "all_videos": "Toutes les vidÊos", "allow_dark_mode": "Autoriser le mode sombre", "allow_edits": "Autoriser les modifications", - "allow_public_user_to_download": "Permettre aux utilisateurs non connectÊs de tÊlÊcharger", - "allow_public_user_to_upload": "Autoriser l'envoi aux utilisateurs non connectÊs", + "allow_public_user_to_download": "Permettre le tÊlÊchargement par des utilisateurs non connectÊs", + "allow_public_user_to_upload": "Permettre l'envoi par des utilisateurs non connectÊs", "alt_text_qr_code": "Image du code QR", "anti_clockwise": "Sens anti-horaire", "api_key": "ClÊ API", @@ -427,6 +447,7 @@ "app_settings": "Paramètres de l'application", "appears_in": "ApparaÃŽt dans", "archive": "Archiver", + "archive_action_prompt": "{count} ajoutÊ(s) à l'archive", "archive_or_unarchive_photo": "Archiver ou dÊsarchiver une photo", "archive_page_no_archived_assets": "Aucun ÊlÊment archivÊ n'a ÊtÊ trouvÊ", "archive_page_title": "Archiver ({count})", @@ -464,7 +485,6 @@ "assets": "MÊdias", "assets_added_count": "{count, plural, one {# mÊdia ajoutÊ} other {# mÊdias ajoutÊs}}", "assets_added_to_album_count": "{count, plural, one {# mÊdia ajoutÊ} other {# mÊdias ajoutÊs}} à l'album", - "assets_added_to_name_count": "{count, plural, one {# mÊdia ajoutÊ} other {# mÊdias ajoutÊs}} à {hasName, select, true {{name}} other {new album}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Le mÊdia ne peut pas ÃĒtre ajoutÊ} other {Les mÊdias ne peuvent pas ÃĒtre ajoutÊs}} à l'album", "assets_count": "{count, plural, one {# mÊdia} other {# mÊdias}}", "assets_deleted_permanently": "{count} mÊdia(s) supprimÊ(s) dÊfinitivement", @@ -553,6 +573,8 @@ "backup_options_page_title": "Options de sauvegarde", "backup_setting_subtitle": "Ajuster les paramètres d'envoi au premier et en arrière-plan", "backward": "Arrière", + "beta_sync": "Statut de la synchronisation bÊta", + "beta_sync_subtitle": "GÊrer le nouveau système de synchronisation", "biometric_auth_enabled": "Authentification biomÊtrique activÊe", "biometric_locked_out": "L'authentification biomÊtrique est verrouillÊ", "biometric_no_options": "Aucune option biomÊtrique disponible", @@ -587,6 +609,7 @@ "cancel": "Annuler", "cancel_search": "Annuler la recherche", "canceled": "AnnulÊ", + "canceling": "Annulation", "cannot_merge_people": "Impossible de fusionner les personnes", "cannot_undo_this_action": "Vous ne pouvez pas annuler cette action !", "cannot_update_the_description": "Impossible de mettre à jour la description", @@ -703,7 +726,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Sombre", - "darkTheme": "Basculer sur le thème sombre", + "dark_theme": "Activer le thème sombre", "date_after": "Date après", "date_and_time": "Date et heure", "date_before": "Date avant", @@ -719,6 +742,7 @@ "default_locale": "RÊgion par dÊfaut", "default_locale_description": "Afficher les dates et nombres en fonction des paramètres de votre navigateur", "delete": "Supprimer", + "delete_action_prompt": "{count} supprimÊ(s) dÊfinitivement", "delete_album": "Supprimer l'album", "delete_api_key_prompt": "Voulez-vous vraiment supprimer cette clÊ API ?", "delete_dialog_alert": "Ces mÊdias seront dÊfinitivement supprimÊs de Immich et de votre appareil", @@ -732,6 +756,7 @@ "delete_key": "Supprimer la clÊ", "delete_library": "Supprimer la bibliothèque", "delete_link": "Supprimer le lien", + "delete_local_action_prompt": "{count} supprimÊ(s) localement", "delete_local_dialog_ok_backed_up_only": "Suppression des donnÊes sauvegardÊes uniquement", "delete_local_dialog_ok_force": "Supprimer tout de mÃĒme", "delete_others": "Supprimer les autres", @@ -745,6 +770,7 @@ "description": "Description", "description_input_hint_text": "Ajouter une description...", "description_input_submit_error": "Erreur de mise à jour de la description, vÊrifier le journal pour plus de dÊtails", + "deselect_all": "Tout dÊsÊlectionner", "details": "DÊtails", "direction": "Ordre", "disabled": "DÊsactivÊ", @@ -762,6 +788,7 @@ "documentation": "Documentation", "done": "TerminÊ", "download": "TÊlÊcharger", + "download_action_prompt": "TÊlÊchargement de {count} ÊlÊments", "download_canceled": "TÊlÊchargement annulÊ", "download_complete": "TÊlÊchargement terminÊ", "download_enqueue": "TÊlÊchargement en attente", @@ -799,6 +826,7 @@ "edit_key": "Modifier la clÊ", "edit_link": "Modifier le lien", "edit_location": "Modifier la localisation", + "edit_location_action_prompt": "{count} localisation(s) mise(s) à jour", "edit_location_dialog_title": "Localisation", "edit_name": "Modifier le nom", "edit_people": "Modifier les personnes", @@ -817,6 +845,7 @@ "empty_trash": "Vider la corbeille", "empty_trash_confirmation": "Êtes-vous sÃģr de vouloir vider la corbeille ? Cela supprimera dÊfinitivement de Immich tous les mÊdias qu'elle contient.\nVous ne pouvez pas annuler cette action !", "enable": "Active", + "enable_backup": "Activer Backup", "enable_biometric_auth_description": "Entrez votre code PIN pour activer l'authentification biomÊtrique", "enabled": "ActivÊ", "end_date": "Date de fin", @@ -984,6 +1013,7 @@ "failed_to_load_assets": "Échec du chargement des ressources", "failed_to_load_folder": "Échec de chargement du dossier", "favorite": "Favori", + "favorite_action_prompt": "{count} ajoutÊ(s) aux Favoris", "favorite_or_unfavorite_photo": "Ajouter ou supprimer des favoris", "favorites": "Favoris", "favorites_page_no_favorites": "Aucun ÊlÊment favori n'a ÊtÊ trouvÊ", @@ -1023,6 +1053,9 @@ "haptic_feedback_switch": "Activer le retour haptique", "haptic_feedback_title": "Retour haptique", "has_quota": "Quota", + "hash_asset": "Hasher le mÊdia", + "hashed_assets": "MÊdia hashÊs", + "hashing": "Hash", "header_settings_add_header_tip": "Ajouter un en-tÃĒte", "header_settings_field_validator_msg": "Cette valeur ne peut pas ÃĒtre vide", "header_settings_header_name_input": "Nom de l'en-tÃĒte", @@ -1036,7 +1069,7 @@ "hide_password": "Masquer le mot de passe", "hide_person": "Masquer la personne", "hide_unnamed_people": "Cacher les personnes non nommÊes", - "home_page_add_to_album_conflicts": "{added} ÊlÊments ajoutÊs à l'album {album}. Les ÊlÊments {failed} sont dÊjà dans l'album.", + "home_page_add_to_album_conflicts": "{added} ÊlÊments ajoutÊs à l'album {album}. {failed} ÊlÊments sont dÊjà dans l'album.", "home_page_add_to_album_err_local": "Impossible d'ajouter des mÊdias locaux aux albums, ils sont ignorÊs", "home_page_add_to_album_success": "{added} ÊlÊments ajoutÊs à l'album {album}.", "home_page_album_err_partner": "Impossible d'ajouter des mÊdias d'un partenaire à un album, ils sont ignorÊs", @@ -1127,6 +1160,7 @@ "library_page_sort_created": "CrÊations les plus rÊcentes", "library_page_sort_last_modified": "Dernière modification", "library_page_sort_title": "Titre de l'album", + "licenses": "Licences", "light": "Clair", "like_deleted": "RÊaction ÂĢ j'aime Âģ supprimÊe", "link_motion_video": "Lier la photo animÊe", @@ -1136,7 +1170,9 @@ "list": "Liste", "loading": "Chargement", "loading_search_results_failed": "Chargement des rÊsultats ÊchouÊ", + "local": "Local", "local_asset_cast_failed": "Impossible de caster un mÊdia qui n'a pas envoyÊ vers le serveur", + "local_assets": "MÊdia locaux", "local_network": "RÊseau local", "local_network_sheet_info": "L'application va se connecter au serveur via cette URL quand l'appareil est connectÊ à ce rÊseau Wi-Fi", "location_permission": "Autorisation de localisation", @@ -1246,6 +1282,7 @@ "more": "Plus", "move": "DÊplacer", "move_off_locked_folder": "DÊplacer en dehors du dossier verrouillÊ", + "move_to_lock_folder_action_prompt": "{count} ajoutÊ(s) au dossier verrouillÊ", "move_to_locked_folder": "DÊplacer dans le dossier verrouillÊ", "move_to_locked_folder_confirmation": "Ces photos et vidÊos seront retirÊs de tout les albums et ne seront visibles que dans le dossier verrouillÊ", "moved_to_archive": "{count, plural, one {# ÊlÊment dÊplacÊ} other {# ÊlÊments dÊplacÊs}} vers les archives", @@ -1292,6 +1329,7 @@ "no_results": "Aucun rÊsultat", "no_results_description": "Essayez un synonyme ou un mot-clÊ plus gÊnÊral", "no_shared_albums_message": "CrÊer un album pour partager vos photos et vidÊos avec les personnes de votre rÊseau", + "no_uploads_in_progress": "Pas d'envoi en cours", "not_in_any_album": "Dans aucun album", "not_selected": "Non sÊlectionnÊ", "note_apply_storage_label_to_previously_uploaded assets": "Note : Pour appliquer l'Êtiquette de stockage aux mÊdias prÊcÊdemment envoyÊs, exÊcutez", @@ -1329,6 +1367,7 @@ "original": "original", "other": "Autre", "other_devices": "Autres appareils", + "other_entities": "Autres entitÊs", "other_variables": "Autres variables", "owned": "PossÊdÊ", "owner": "PropriÊtaire", @@ -1390,7 +1429,7 @@ "photos_and_videos": "Photos et vidÊos", "photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}", "photos_from_previous_years": "Photos des annÊes prÊcÊdentes", - "pick_a_location": "Choisissez un lieu", + "pick_a_location": "Choisissez une localisation", "pin_code_changed_successfully": "Code PIN changÊ avec succès", "pin_code_reset_successfully": "RÊinitialisation du code PIN rÊussie", "pin_code_setup_successfully": "DÊfinition du code PIN rÊussie", @@ -1460,6 +1499,7 @@ "purchase_server_description_2": "Statut de contributeur", "purchase_server_title": "Serveur", "purchase_settings_server_activated": "La clÊ du produit pour le Serveur est gÊrÊe par l'administrateur", + "queue_status": "File d'attente {count}/{total}", "rating": "Étoile d'Êvaluation", "rating_clear": "Effacer l'Êvaluation", "rating_count": "{count, plural, one {# Êtoile} other {# Êtoiles}}", @@ -1488,6 +1528,8 @@ "refreshing_faces": "Actualisation des visages", "refreshing_metadata": "Actualisation des mÊtadonnÊes", "regenerating_thumbnails": "RegÊnÊration des miniatures", + "remote": "A distance", + "remote_assets": "MÊdia à distance", "remove": "Supprimer", "remove_assets_album_confirmation": "Êtes-vous sÃģr de vouloir supprimer {count, plural, one {# mÊdia} other {# mÊdias}} de l'album ?", "remove_assets_shared_link_confirmation": "Êtes-vous sÃģr de vouloir supprimer {count, plural, one {# mÊdia} other {# mÊdias}} de ce lien partagÊ ?", @@ -1495,7 +1537,9 @@ "remove_custom_date_range": "Supprimer la plage de date personnalisÊe", "remove_deleted_assets": "Supprimer les fichiers hors ligne", "remove_from_album": "Supprimer de l'album", + "remove_from_album_action_prompt": "{count} supprimÊ(s) de l'album", "remove_from_favorites": "Supprimer des favoris", + "remove_from_lock_folder_action_prompt": "{count} supprimÊ(s) du dossier verrouillÊ", "remove_from_locked_folder": "Supprimer du dossier verrouillÊ", "remove_from_locked_folder_confirmation": "Êtes vous sÃģr de vouloir dÊplacer ces photos et vidÊos en dehors du dossier verrouillÊ ? Elles seront visibles dans votre galerie.", "remove_from_shared_link": "Supprimer des liens partagÊs", @@ -1510,7 +1554,7 @@ "removed_from_favorites_count": "{count, plural, one {# supprimÊ} other {# supprimÊs}} des favoris", "removed_memory": "Souvenir supprimÊ", "removed_photo_from_memory": "Photo supprimÊe du souvenir", - "removed_tagged_assets": "Tag supprimÊ de {count, plural, one {# mÊdia} other {# mÊdias}}", + "removed_tagged_assets": "Étiquette supprimÊe de {count, plural, one {# mÊdia} other {# mÊdias}}", "rename": "Renommer", "repair": "RÊparer", "repair_no_results_message": "Les fichiers non importÊs ou absents s'afficheront ici", @@ -1523,11 +1567,15 @@ "reset_password": "RÊinitialiser le mot de passe", "reset_people_visibility": "RÊinitialiser la visibilitÊ des personnes", "reset_pin_code": "RÊinitialiser le code PIN", + "reset_sqlite": "RÊinitialiser la base de donnÊes SQLite", + "reset_sqlite_confirmation": "Êtes vous sur que vous voulez rÊinitialiser la base de donnÊes SQLite ? Vous devrez vous dÊconnecter and vous reconnecter à nouveau pour re-synchroniser les donnÊes", + "reset_sqlite_success": "La base de donnÊes SQLite à ÊtÊ rÊinitialisÊ avec succès", "reset_to_default": "RÊtablir les valeurs par dÊfaut", "resolve_duplicates": "RÊsoudre les doublons", "resolved_all_duplicates": "RÊsolution de tous les doublons", "restore": "Restaurer", "restore_all": "Tout restaurer", + "restore_trash_action_prompt": "{count} restaurÊ de la corbeille", "restore_user": "Restaurer l'utilisateur", "restored_asset": "MÊdia restaurÊ", "resume": "Reprendre", @@ -1536,6 +1584,7 @@ "role": "Rôle", "role_editor": "Éditeur", "role_viewer": "Visionneuse", + "running": "En marche", "save": "Sauvegarder", "save_to_gallery": "Enregistrer", "saved_api_key": "ClÊ API sauvegardÊe", @@ -1566,8 +1615,8 @@ "search_filter_display_option_not_in_album": "Pas dans un album", "search_filter_display_options": "Options d'affichage", "search_filter_filename": "Recherche par nom de fichier", - "search_filter_location": "Lieu", - "search_filter_location_title": "SÊlectionner un lieu", + "search_filter_location": "Localisation", + "search_filter_location_title": "SÊlectionner une localisation", "search_filter_media_type": "Type de mÊdia", "search_filter_media_type_title": "SÊlectionner type de mÊdia", "search_filter_people_title": "SÊlectionner une personne", @@ -1667,6 +1716,7 @@ "settings_saved": "Paramètres sauvegardÊs", "setup_pin_code": "DÊfinir un code PIN", "share": "Partager", + "share_action_prompt": "{count} ÊlÊments partagÊs", "share_add_photos": "Ajouter des photos", "share_assets_selected": "{count} sÊlectionnÊ(s)", "share_dialog_preparing": "PrÊparation...", @@ -1768,6 +1818,7 @@ "sort_title": "Titre", "source": "Source", "stack": "Empiler", + "stack_action_prompt": "{count} groupÊ(s)", "stack_duplicates": "Empiler les doublons", "stack_select_one_photo": "SÊlectionnez une photo principale pour la pile", "stack_selected_photos": "Empiler les photos sÊlectionnÊes", @@ -1787,6 +1838,7 @@ "storage_quota": "Quota de stockage", "storage_usage": "{used} sur {available} utilisÊ", "submit": "Soumettre", + "success": "RÊussi", "suggestions": "Suggestions", "sunrise_on_the_beach": "Lever de soleil sur la plage", "support": "Soutenir", @@ -1796,6 +1848,8 @@ "sync": "Synchroniser", "sync_albums": "Synchroniser dans des albums", "sync_albums_manual_subtitle": "Synchroniser toutes les vidÊos et photos envoyÊes dans les albums sÊlectionnÊs", + "sync_local": "Synchronisation locale", + "sync_remote": "Synchronisation à distance", "sync_upload_album_setting_subtitle": "CrÊez et envoyez vos photos et vidÊos dans les albums sÊlectionnÊs sur Immich", "tag": "Étiquette", "tag_assets": "Étiqueter les mÊdias", @@ -1806,6 +1860,7 @@ "tag_updated": "Étiquette mise à jour : {tag}", "tagged_assets": "Étiquette ajoutÊe à {count, plural, one {# mÊdia} other {# mÊdias}}", "tags": "Étiquettes", + "tap_to_run_job": "Appuyez pour dÊmarrer la tÃĸche", "template": "Modèle", "theme": "Thème", "theme_selection": "SÊlection du thème", @@ -1838,6 +1893,7 @@ "total": "Total", "total_usage": "Utilisation globale", "trash": "Corbeille", + "trash_action_prompt": "{count} mis à la corbeille", "trash_all": "Tout supprimer", "trash_count": "Corbeille {count, number}", "trash_delete_asset": "Mettre à la corbeille/Supprimer un mÊdia", @@ -1855,9 +1911,11 @@ "unable_to_change_pin_code": "Impossible de changer le code PIN", "unable_to_setup_pin_code": "Impossible de dÊfinir le code PIN", "unarchive": "DÊsarchiver", + "unarchive_action_prompt": "{count} supprimÊ(s) de l'archive", "unarchived_count": "{count, plural, one {# supprimÊ} other {# supprimÊs}} de l'archive", "undo": "Annuler", "unfavorite": "Enlever des favoris", + "unfavorite_action_prompt": "{count} supprimÊ(s) des favoris", "unhide_person": "Afficher la personne", "unknown": "Inconnu", "unknown_country": "Pays non connu", @@ -1875,12 +1933,15 @@ "unselect_all_duplicates": "DÊsÊlectionner tous les doublons", "unselect_all_in": "Tout dÊsÊlectionner dans {group}", "unstack": "DÊsempiler", + "unstack_action_prompt": "{count} non groupÊs", "unstacked_assets_count": "{count, plural, one {# mÊdia dÊpilÊ} other {# mÊdias dÊpilÊs}}", + "untagged": "Étiquette supprimÊe", "up_next": "Suite", "updated_at": "Mis à jour à", "updated_password": "Mot de passe mis à jour", "upload": "Envoyer", "upload_concurrency": "Envois simultanÊs", + "upload_details": "Uploader les details", "upload_dialog_info": "Voulez-vous sauvegarder la sÊlection vers le serveur ?", "upload_dialog_title": "Envoyer le mÊdia", "upload_errors": "L'envoi s'est complÊtÊ avec {count, plural, one {# erreur} other {# erreurs}}. RafraÃŽchissez la page pour voir les nouveaux mÊdias envoyÊs.", @@ -1912,6 +1973,7 @@ "user_usage_stats_description": "Voir les statistiques d'utilisation du compte", "username": "Nom d'utilisateur", "users": "Utilisateurs", + "users_added_to_album_count": "{count, plural, one {# utilisateur ajoutÊ} other {# utilisateurs ajoutÊs}} à l'album", "utilities": "Utilitaires", "validate": "Valider", "validate_endpoint_error": "Merci d'entrer un lien valide", @@ -1930,6 +1992,7 @@ "view_album": "Afficher l'album", "view_all": "Voir tout", "view_all_users": "Voir tous les utilisateurs", + "view_details": "Voir les dÊtails", "view_in_timeline": "Voir dans la vue chronologique", "view_link": "Voir le lien", "view_links": "Voir les liens", diff --git a/i18n/gl.json b/i18n/gl.json index 558cc48900..70146abb53 100644 --- a/i18n/gl.json +++ b/i18n/gl.json @@ -455,7 +455,6 @@ "assets": "Activos", "assets_added_count": "Engadido {count, plural, one {# activo} other {# activos}}", "assets_added_to_album_count": "Engadido {count, plural, one {# activo} other {# activos}} ao ÃĄlbum", - "assets_added_to_name_count": "Engadido {count, plural, one {# activo} other {# activos}} a {hasName, select, true {{name}} other {novo ÃĄlbum}}", "assets_count": "{count, plural, one {# activo} other {# activos}}", "assets_deleted_permanently": "{count} activo(s) eliminado(s) permanentemente", "assets_deleted_permanently_from_server": "{count} activo(s) eliminado(s) permanentemente do servidor Immich", diff --git a/i18n/he.json b/i18n/he.json index 737c307f31..49973dbc63 100644 --- a/i18n/he.json +++ b/i18n/he.json @@ -34,6 +34,7 @@ "added_to_favorites_count": "{count, number} נוספו למו×ĸדפים", "admin": { "add_exclusion_pattern_description": "הוספ×Ē ×“×¤×•×Ą×™ החרגה. × ×Ēמכ×Ē ×”×Ēאמ×Ē ×“×¤×•×Ą×™× באמ×Ļ×ĸו×Ē *, ** ו-?. כדי לה×Ē×ĸלם מכל הקב×Ļים ב×Ēיקיה כלשהי בשם \"Raw\", יש להש×Ēמ׊ ב \"**/Raw/**\". כדי לה×Ē×ĸלם מכל הקב×Ļים המס×Ēיימים ב \"tif.\", יש להש×Ēמ׊ ב \"tif.*/**\". כדי לה×Ē×ĸלם מנ×Ēיב מוחלט, יש להש×Ēמ׊ ב \"**/× ×Ēיב/לה×Ē×ĸלמו×Ē\".", + "admin_user": "מנהל מ×ĸרכ×Ē", "asset_offline_description": "×Ēמונה מספרייה חי×Ļוני×Ē ×–×• לא נמ×Ļא×Ē ×™×•×Ēר בדיסק והו×ĸברה לאשפה. אם הקוב×Ĩ הו×ĸבר מ×Ēוך הספרייה, נא לבדוק א×Ē ×Ļיר הזמן שלך ×ĸבור ה×Ēמונה המקבילה החדש. כדי לשחזר ×Ēמונה זו, נא לוודא ׊-Immich יכול לגש×Ē ××œ × ×Ēיב הקוב×Ĩ למטה ולסרוק מחדש א×Ē ×”×Ą×¤×¨×™×™×”.", "authentication_settings": "הגדרו×Ē ×”×Ēחברו×Ē", "authentication_settings_description": "ניהול סיסמה, OAuth, והגדרו×Ē ×”×Ēחברו×Ē ××—×¨×•×Ē", @@ -165,6 +166,20 @@ "metadata_settings_description": "ניהול הגדרו×Ē ×ž×˜×-× ×Ēונים", "migration_job": "ה×ĸברה", "migration_job_description": "ה×ĸבר ×Ēמונו×Ē ×ž×ž×•×–×ĸרו×Ē ×Š×œ ×Ēמונו×Ē ×•×¤× ×™× למבנה ה×Ēיקיו×Ē ×”×ĸדכני ביו×Ēר", + "nightly_tasks_cluster_faces_setting_description": "ב×Ļ×ĸ זיהוי פנים ×ĸבור פר×Ļופים שזוהו לאחרונה", + "nightly_tasks_cluster_new_faces_setting": "קב×Ĩ פנים חדשו×Ē", + "nightly_tasks_database_cleanup_setting": "משימו×Ē ×Ēחזוקה וניקוי של מסד הנ×Ēונים", + "nightly_tasks_database_cleanup_setting_description": "נקה × ×Ēונים ישנים שפג ×Ēוקפם ממסד הנ×Ēונים", + "nightly_tasks_generate_memories_setting": "י×Ļיר×Ē ×–×›×¨×•× ×•×Ē", + "nightly_tasks_generate_memories_setting_description": "×Ļור זכרונו×Ē ×—×“×Š×™× מה×Ēמונו×Ē ×Š×œ×š", + "nightly_tasks_missing_thumbnails_setting": "×Ļור ×Ēמונו×Ē ×ž×ž×•×–×ĸרו×Ē ×—×Ą×¨×•×Ē", + "nightly_tasks_missing_thumbnails_setting_description": "×”×•×Ą×Ŗ ל×Ēור קב×Ļים ללא ×Ēמונו×Ē ×ž×ž×•×–×ĸרו×Ē ×œ×™×Ļירה של ×Ēמונו×Ē ×ž×ž×•×–×ĸרו×Ē", + "nightly_tasks_settings": "הגדרו×Ē ×Š×œ משימו×Ē ×œ×™×œ×™×•×Ē", + "nightly_tasks_settings_description": "נהל משימו×Ē ×œ×™×œ×™×•×Ē", + "nightly_tasks_start_time_setting": "זמן ה×Ēחלה", + "nightly_tasks_start_time_setting_description": "הש×ĸה שבה השר×Ē ×ž×Ēחיל להרי×Ĩ א×Ē ×”×ž×Š×™×ž×•×Ē ×”×œ×™×œ×™×•×Ē", + "nightly_tasks_sync_quota_usage_setting": "סנכרן א×Ē ×”×Š×™×ž×•×Š באחסון", + "nightly_tasks_sync_quota_usage_setting_description": "×ĸדכן א×Ē ×ž×›×Ą×Ē ×”××—×Ą×•×Ÿ של המש×Ēמ׊ בה×Ēאם לשימוש הנוכחי", "no_paths_added": "לא נוספו × ×Ēיבים", "no_pattern_added": "לא נוספה ×Ēבני×Ē", "note_apply_storage_label_previous_assets": "ה×ĸרה: כדי להחיל א×Ē ×Ēווי×Ē ×”××—×Ą×•×Ÿ ×ĸל ×Ēמונו×Ē ×Š×”×•×ĸלו ב×ĸבר, הפ×ĸל א×Ē", @@ -203,7 +218,7 @@ "oauth_storage_quota_claim": "דריש×Ē ×ž×›×Ą×Ē ××—×Ą×•×Ÿ", "oauth_storage_quota_claim_description": "הגדר אוטומטי×Ē ××Ē ×ž×›×Ą×Ē ×”××—×Ą×•×Ÿ של המש×Ēמ׊ ל×ĸרך של דרישה זו.", "oauth_storage_quota_default": "מכס×Ē ××—×Ą×•×Ÿ בריר×Ē ×ž×—×“×œ (GiB)", - "oauth_storage_quota_default_description": "מכסה ב-GiB לשימוש כאשר לא מסופק×Ē ×“×¨×™×Š×” (הזן 0 ×ĸבור מכסה בל×Ēי מוגבל×Ē).", + "oauth_storage_quota_default_description": "מכסה ב-GiB לשימוש כאשר לא מסופק×Ē ×“×¨×™×Š×”.", "oauth_timeout": "הבקשה נכשלה – הזמן הק×Ļוב הס×Ēיים", "oauth_timeout_description": "זמן ×§×Ļוב לבקשו×Ē (במילישניו×Ē)", "password_enable_description": "ה×Ēחבר ×ĸם דוא\"ל וסיסמה", @@ -243,6 +258,7 @@ "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": "ניהול מבנה ה×Ēיקיו×Ē ×•××Ē ×Š× הקוב×Ĩ של ה×Ēמונה שהו×ĸל×Ēה", @@ -425,6 +441,7 @@ "app_settings": "הגדרו×Ē ×™×™×Š×•×", "appears_in": "מופי×ĸ ב", "archive": "ארכיון", + "archive_action_prompt": "{count} נוספו לארכיון", "archive_or_unarchive_photo": "ה×ĸבר ×Ēמונה לארכיון או הו×Ļא או×Ēה מ׊ם", "archive_page_no_archived_assets": "לא נמ×Ļאו ×Ēמונו×Ē ×‘××¨×›×™×•×Ÿ", "archive_page_title": "בארכיון ({count})", @@ -462,7 +479,6 @@ "assets": "×Ēמונו×Ē", "assets_added_count": "{count, plural, one {נוספה ×Ēומנה #} other {נוספו # ×Ēמונו×Ē}}", "assets_added_to_album_count": "{count, plural, one {נוספה ×Ēמונה #} other {נוספו # ×Ēמונו×Ē}} לאלבום", - "assets_added_to_name_count": "{count, plural, one {×Ēמונה # נוספה} other {# ×Ēמונו×Ē × ×•×Ą×¤×•}} אל {hasName, select, true {{name}} other {אלבום חדש}}", "assets_cannot_be_added_to_album_count": "לא ני×Ēן ×œ×”×•×Ą×™×Ŗ א×Ē ×”{count, plural, one {×Ēמונה} other {×Ēמונו×Ē}} לאלבום", "assets_count": "{count, plural, one {×Ēמונה #} other {# ×Ēמונו×Ē}}", "assets_deleted_permanently": "{count} ×Ēמונו×Ē × ×ž×—×§×• ל×Ļמי×Ēו×Ē", @@ -701,7 +717,7 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "כהה", - "darkTheme": "החלפה למ×Ļב חושך", + "dark_theme": "הפ×ĸל/כבה מ×Ļב כהה", "date_after": "×Ēאריך אחרי", "date_and_time": "×Ēאריך וש×ĸה", "date_before": "×Ēאריך לפני", @@ -711,12 +727,13 @@ "day": "יום", "deduplicate_all": "ביטול כל הכפילויו×Ē", "deduplication_criteria_1": "גודל ×Ēמונה בב×Ēים", - "deduplication_criteria_2": "ספיר×Ē × ×Ēוני EXIF", + "deduplication_criteria_2": "כמו×Ē × ×Ēוני EXIF", "deduplication_info": "מיד×ĸ ×ĸל ביטול כפילויו×Ē", "deduplication_info_description": "כדי לבחור מרא׊ ×Ēמונו×Ē ×‘××•×¤×Ÿ אוטומטי ולהסיר כפילויו×Ē ×‘×›×ž×•×Ē ×’×“×•×œ×”, אנו מס×Ēכלים ×ĸל:", "default_locale": "׊פ×Ē ×‘×¨×™×¨×Ē ×ž×—×“×œ", "default_locale_description": "פורמט ×Ēאריכים ומספרים מבוסס ׊פ×Ē ×”×“×¤×“×¤×Ÿ שלך", "delete": "מחק", + "delete_action_prompt": "{count} נמחקו ל×Ļמי×Ēו×Ē", "delete_album": "מחק אלבום", "delete_api_key_prompt": "האם א×Ēה בטוח שבר×Ļונך למחוק מפ×Ēח ה-API הזה?", "delete_dialog_alert": "הפריטים האלה ימחקו ל×Ļמי×Ēו×Ē ×ž×”×Š×¨×Ē ×•×ž×”×ž×›×Š×™×¨ שלך", @@ -797,6 +814,7 @@ "edit_key": "×ĸרוך מפ×Ēח", "edit_link": "×ĸרוך קישור", "edit_location": "×ĸרוך מיקום", + "edit_location_action_prompt": "{count} מיקומים × ×ĸרכו", "edit_location_dialog_title": "מיקום", "edit_name": "×ĸרוך ׊ם", "edit_people": "×ĸרוך אנשים", @@ -982,6 +1000,7 @@ "failed_to_load_assets": "ט×ĸינ×Ē ×Ēמונו×Ē × ×›×Š×œ×”", "failed_to_load_folder": "ט×ĸינ×Ē ×Ēיקיה נכשלה", "favorite": "מו×ĸדת", + "favorite_action_prompt": "{count} נוספו למו×ĸדפים", "favorite_or_unfavorite_photo": "×”×•×Ą×Ŗ או הסר ×Ēמונה מהמו×ĸדפים", "favorites": "מו×ĸדפים", "favorites_page_no_favorites": "לא נמ×Ļאו ×Ēמונו×Ē ×ž×•×ĸדפים", @@ -1148,6 +1167,7 @@ "locked_folder": "×Ēיקיה × ×ĸולה", "log_out": "ה×Ē× ×Ē×§", "log_out_all_devices": "ה×Ē× ×Ē×§ מכל המכשירים", + "logged_in_as": "מחובר כ {user}", "logged_out_all_devices": "מנו×Ē×§ מכל המכשירים", "logged_out_device": "מכשיר מנו×Ē×§", "login": "כניסה", @@ -1243,6 +1263,7 @@ "more": "×ĸוד", "move": "ה×ĸבר", "move_off_locked_folder": "הו×Ļאה מה×Ēיקייה הנ×ĸולה", + "move_to_lock_folder_action_prompt": "{count} נוספו ל×Ēיקייה הנ×ĸולה", "move_to_locked_folder": "ה×ĸבר ל×Ēיקיה הנ×ĸולה", "move_to_locked_folder_confirmation": "ה×Ēמונו×Ē ×•×”×Ą×¨×˜×•× ×™× האלו יוסרו מכל האלבומים, ויהיו מו×Ļגים רק ב×Ēיקיה הנ×ĸולה", "moved_to_archive": "{count, plural, one {הו×ĸברה ×Ēמונה # } other {# ×Ēמונו×Ē ×”×•×ĸברו}} לארכיון", @@ -1492,7 +1513,9 @@ "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": "הסר מקישור משו×Ē×Ŗ", @@ -1605,6 +1628,7 @@ "select_album_cover": "בחר ×ĸטיפ×Ē ××œ×‘×•×", "select_all": "בחר הכל", "select_all_duplicates": "בחר א×Ē ×›×œ הכפילויו×Ē", + "select_all_in": "בחר הכול ב×Ēוך {group}", "select_avatar_color": "בחר ×Ļב×ĸ ×Ēמונ×Ē ×¤×¨×•×¤×™×œ", "select_face": "בחר פנים", "select_featured_photo": "בחר ×Ēמונה מיי×Ļג×Ē", @@ -1834,6 +1858,7 @@ "total": "סה\"כ", "total_usage": "שימוש כולל", "trash": "אשפה", + "trash_action_prompt": "{count} הו×ĸברו לאשפה", "trash_all": "ה×ĸבר הכל לאשפה", "trash_count": "ה×ĸבר לאשפה {count, number}", "trash_delete_asset": "ה×ĸבר לאשפה/מחק ×Ēמונה", @@ -1851,9 +1876,11 @@ "unable_to_change_pin_code": "לא ני×Ēן לשנו×Ē ××Ē ×§×•×“ ה PIN", "unable_to_setup_pin_code": "לא ני×Ēן להגדיר קוד PIN", "unarchive": "הו×Ļא מארכיון", + "unarchive_action_prompt": "{count} הוסרו מהארכיון", "unarchived_count": "{count, plural, other {# הו×Ļאו מהארכיון}}", "undo": "לבטל", "unfavorite": "לא מו×ĸדת", + "unfavorite_action_prompt": "{count} הוסרו מהמו×ĸדפים", "unhide_person": "בטל הס×Ēר×Ē ××“×", "unknown": "לא ידו×ĸ", "unknown_country": "מדינה לא ידו×ĸה", @@ -1869,8 +1896,10 @@ "unsaved_change": "שינוי לא נ׊מר", "unselect_all": "בטל בחירה בהכל", "unselect_all_duplicates": "בטל בחיר×Ē ×›×œ הכפילויו×Ē", + "unselect_all_in": "בטל א×Ē ×”×‘×—×™×¨×” של הכל ב {group}", "unstack": "בטל ×ĸרימה", "unstacked_assets_count": "{count, plural, one {×Ēמונה # הוסרה} other {# ×Ēמונו×Ē ×”×•×Ą×¨×•}} מה×ĸרימה", + "untagged": "לא מ×Ēיוגים", "up_next": "הבא ב×Ēור", "updated_at": "×ĸודכן", "updated_password": "סיסמה ×ĸודכנה", diff --git a/i18n/hi.json b/i18n/hi.json index abb1552154..2cb29370b5 100644 --- a/i18n/hi.json +++ b/i18n/hi.json @@ -22,6 +22,7 @@ "add_partner": "⤜āĨ‹ā¤Ąā¤ŧāĨ€ā¤Ļā¤žā¤° ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "add_path": "ā¤Ēā¤Ĩ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "add_photos": "ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_tag": "⤚ā¤ŋā¤šāĨā¤¨ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "add_to": "ā¤‡ā¤¸ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛āĨ‡ā¤‚â€Ļ", "add_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "add_to_album_bottom_sheet_added": "{album} ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", @@ -33,6 +34,7 @@ "added_to_favorites_count": "ā¤Ē⤏⤂ā¤ĻāĨ€ā¤Ļā¤ž ā¤ŽāĨ‡ā¤‚ {count, number} ā¤Ąā¤žā¤˛ā¤ž ā¤—ā¤¯ā¤ž", "admin": { "add_exclusion_pattern_description": "ā¤Ŧā¤šā¤ŋ⤎āĨā¤•⤰⤪ ā¤ĒāĨˆā¤Ÿā¤°āĨā¤¨ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚. *, **, ⤔⤰ ? ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰⤕āĨ‡ ⤗āĨā¤˛āĨ‹ā¤Ŧā¤ŋ⤂⤗ ā¤•ā¤°ā¤¨ā¤ž ā¤¸ā¤Žā¤°āĨā¤Ĩā¤ŋ⤤ ā¤šāĨˆāĨ¤ \"Raw\" ā¤¨ā¤žā¤Žā¤• ⤕ā¤ŋ⤏āĨ€ ⤭āĨ€ ⤍ā¤ŋ⤰āĨā¤ĻāĨ‡ā¤ļā¤ŋā¤•ā¤ž ⤕āĨ€ ⤏⤭āĨ€ ā¤Ģā¤ŧā¤žā¤‡ā¤˛āĨ‹ā¤‚ ⤕āĨ‹ ⤅⤍ā¤ĻāĨ‡ā¤–ā¤ž ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤, \"**/Raw/**\" ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰āĨ‡ā¤‚āĨ¤ \".tif\" ⤏āĨ‡ ā¤¸ā¤Žā¤žā¤ĒāĨā¤¤ ā¤šāĨ‹ā¤¨āĨ‡ ā¤ĩā¤žā¤˛āĨ€ ⤏⤭āĨ€ ā¤Ģā¤ŧā¤žā¤‡ā¤˛āĨ‹ā¤‚ ⤕āĨ‹ ⤅⤍ā¤ĻāĨ‡ā¤–ā¤ž ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤, \"**/*.tif\" ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰āĨ‡ā¤‚āĨ¤ ⤕ā¤ŋ⤏āĨ€ ā¤ĒāĨ‚⤰āĨā¤Ŗ ā¤Ēā¤Ĩ ⤕āĨ‹ ⤅⤍ā¤ĻāĨ‡ā¤–ā¤ž ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤, \"/path/to/ignore/**\" ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰āĨ‡ā¤‚āĨ¤", + "admin_user": "ā¤ĩāĨā¤¯ā¤ĩ⤏āĨā¤Ĩā¤žā¤Ē⤕ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž", "asset_offline_description": "ā¤¯ā¤š ā¤Ŧā¤žā¤šā¤°āĨ€ ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ā¤ā¤¸āĨ‡ā¤Ÿ ⤅ā¤Ŧ ā¤Ąā¤ŋ⤏āĨā¤• ā¤Ē⤰ ā¤ŽāĨŒā¤œāĨ‚ā¤Ļ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆ ⤔⤰ ⤇⤏āĨ‡ ⤟āĨā¤°āĨˆā¤ļ ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ ā¤Ļā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆāĨ¤ ⤝ā¤Ļā¤ŋ ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ⤕āĨ‹ ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ⤕āĨ‡ ⤭āĨ€ā¤¤ā¤° ā¤•ā¤šāĨ€ā¤‚ ⤞āĨ‡ ā¤œā¤žā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤Ĩā¤ž, ⤤āĨ‹ ⤍⤈ ⤏⤂ā¤Ŧ⤂⤧ā¤ŋ⤤ ā¤ā¤¸āĨ‡ā¤Ÿ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤅ā¤Ē⤍āĨ€ ā¤Ÿā¤žā¤‡ā¤Žā¤˛ā¤žā¤‡ā¤¨ ā¤ĻāĨ‡ā¤–āĨ‡ā¤‚āĨ¤ ⤇⤏ ā¤ā¤¸āĨ‡ā¤Ÿ ⤕āĨ‹ ā¤ĩā¤žā¤Ē⤏ ā¤Ēā¤žā¤¨āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤, ⤕āĨƒā¤Ēā¤¯ā¤ž ⤏āĨā¤¨ā¤ŋā¤ļāĨā¤šā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚ ⤕ā¤ŋ ⤍āĨ€ā¤šāĨ‡ ā¤Ļā¤ŋā¤ ā¤—ā¤ ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤Ēā¤Ĩ ⤕āĨ‹ ā¤‡ā¤ŽāĨā¤Žā¤ŋ⤚ ā¤ĻāĨā¤ĩā¤žā¤°ā¤ž ā¤ā¤•āĨā¤¸āĨ‡ā¤¸ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž ā¤šāĨˆ ⤔⤰ ā¤Ģā¤ŋ⤰ ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ⤕āĨ‹ ⤏āĨā¤•āĨˆā¤¨ ⤕⤰āĨ‡ā¤‚āĨ¤", "authentication_settings": "ā¤ĒāĨā¤°ā¤Žā¤žā¤ŖāĨ€ā¤•⤰⤪ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸", "authentication_settings_description": "ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą, OAuth ⤔⤰ ⤅⤍āĨā¤¯ ā¤ĒāĨā¤°ā¤Žā¤žā¤ŖāĨ€ā¤•⤰⤪ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸ ā¤ĒāĨā¤°ā¤Ŧ⤂⤧ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", diff --git a/i18n/hr.json b/i18n/hr.json index d2edfc9a8e..35e7aba7e0 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -462,7 +462,6 @@ "assets": "Sredstva", "assets_added_count": "Dodano {count, plural, one {# asset} other {# assets}}", "assets_added_to_album_count": "Dodano {count, plural, one {# asset} other {# assets}} u album", - "assets_added_to_name_count": "Dodano {count, plural, one {# asset} other {# assets}} u {hasName, select, true {{name}} other {new album}}", "assets_cannot_be_added_to_album_count": "{count, plural,\n one {Nije moguće dodati medij u album}\n few {Nije moguće dodati # medija u album}\n other {Nije moguće dodati # medija u album}\n}", "assets_count": "{count, plural, one {# asset} other {# assets}}", "assets_deleted_permanently": "{count} resurs(i) uspjeÅĄno uklonjeni", diff --git a/i18n/hu.json b/i18n/hu.json index 22ec6f22a1..df995c0d6c 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -22,6 +22,7 @@ "add_partner": "Partner hozzÃĄadÃĄsa", "add_path": "ElÊrÊsi Ãētvonal megadÃĄsa", "add_photos": "FotÃŗk hozzÃĄadÃĄsa", + "add_tag": "Címke hozzÃĄadÃĄsa", "add_to": "HozzÃĄadÃĄs ideâ€Ļ", "add_to_album": "FelvÊtel albumba", "add_to_album_bottom_sheet_added": "HozzÃĄadva a(z) \"{album}\" albumhoz", @@ -33,6 +34,7 @@ "added_to_favorites_count": "{count, number} hozzÃĄadva a kedvencekhez", "admin": { "add_exclusion_pattern_description": "KihagyÃĄsi mintÃĄk (pattern) megadÃĄsa. A *, ** Ês ? helyettesítő karakterek engedÊlyezettek. Pl. a \"Raw\" kÃļnyvtÃĄrban tÃĄrolt Ãļsszes fÃĄjl kihagyÃĄsÃĄhoz hasznÃĄlhatÃŗ a \"**/Raw/**\". Minden \".tif\" fÃĄjl kihagyÃĄsa az Ãļsszes mappÃĄban: \"**/*.tif\". AbszolÃēt elÊrÊsi Ãētvonal kihagyÃĄsa: \"/kihagyni/kivant/mappa/**\".", + "admin_user": "Admin felhasznÃĄlÃŗ", "asset_offline_description": "Ez a kÃŧlső kÊptÃĄrban lÊvő elem mÃĄr nem talÃĄlhatÃŗ, ezÊrt a lomtÃĄrba kerÃŧlt. Ha a fÃĄjl a kÊptÃĄron belÃŧl lett ÃĄthelyezve, akkor ellenőrizd, hogy tovÃĄbbra is lÃĄthatÃŗ az idővonaladon. Az elem visszaÃĄllítÃĄsÃĄhoz győződj meg rÃŗla, hogy az alÃĄbbi mappa az Immich szÃĄmÃĄra elÊrhető, majd Ãējra fÊsÃŧld ÃĄt a kÊptÃĄrat.", "authentication_settings": "HitelesítÊsi beÃĄllítÃĄsok", "authentication_settings_description": "JelszÃŗ, OAuth Ês egyÊb hitelesítÊsi beÃĄllítÃĄsok kezelÊse", @@ -43,7 +45,7 @@ "backup_database_enable_description": "AdatbÃĄzis mentÊsek engedÊlyezÊse", "backup_keep_last_amount": "Megőrizendő korÃĄbbi mentÊsek szÃĄma", "backup_settings": "AdatbÃĄzis mentÊs beÃĄllítÃĄsai", - "backup_settings_description": "AdatbÃĄzis mentÊs beÃĄllítÃĄsainak kezelÊse. MegjegyzÊs: Ezek a feladatok nincsenek felÃŧgyelve, így nem kapsz ÊrtesítÊs meghiÃēsulÃĄs esetÊn.", + "backup_settings_description": "AdatbÃĄzis mentÊs beÃĄllítÃĄsainak kezelÊse.", "cleared_jobs": "{job}: feladatai tÃļrÃļlve", "config_set_by_file": "A konfigurÃĄciÃŗt jelenleg egy konfigurÃĄciÃŗs fÃĄjl ÃĄllítja be", "confirm_delete_library": "Biztosan ki szeretnÊd tÃļrÃļlni a {library} kÊptÃĄrat?", @@ -138,7 +140,7 @@ "machine_learning_smart_search_description": "KÊpek szemantikai keresÊse CLIP beÃĄgyazÃĄsok segítsÊgÊvel", "machine_learning_smart_search_enabled": "Okos keresÊs engedÊlyezÊse", "machine_learning_smart_search_enabled_description": "Ha ki van kapcsolva, a kÊpek nem lesznek ÃĄtalakítva okos keresÊshez.", - "machine_learning_url_description": "GÊpi tanulÃĄs szerver URL címe. Ha tÃļbbi, mint egy URL van megadva, mindegyik szervert egyenkÊnt prÃŗbÃĄlja meg, amíg az egyik sikeresen nem vÃĄlaszol, sorrendben az elsőtől az utÃŗlsÃŗig. A nem vÃĄlaszolÃŗ szervereket ÃĄtmenetileg figyelmen kívÃŧl hagyja, amíg Ãējra online nem lesznek.", + "machine_learning_url_description": "GÊpi tanulÃĄs szerver URL címe. Ha tÃļbbi, mint egy URL van megadva, mindegyik szervert egyenkÊnt prÃŗbÃĄlja meg, amíg az egyik sikeresen nem vÃĄlaszol, sorrendben az elsőtől az utÃŗlsÃŗig. A nem elÊrhető szervereket ÃĄtmenetileg figyelmen kívÃŧl lesznek hagyva, amíg Ãējra online nem lesznek.", "manage_concurrency": "PÃĄrhuzamos Feladatok KezelÊse", "manage_log_settings": "NaplÃŗzÃĄsi beÃĄllítÃĄsok kezelÊse", "map_dark_style": "SÃļtÊt stílus", @@ -155,7 +157,7 @@ "map_settings_description": "TÊrkÊp beÃĄllítÃĄsok kezelÊse", "map_style_description": "Egy style.json tÊrkÊptÊmÃĄra mutatÃŗ URL cím", "memory_cleanup_job": "MemÃŗria takarítÃĄs", - "memory_generate_job": "EmlÊk generÃĄlÃĄlsa", + "memory_generate_job": "EmlÊk generÃĄlÃĄsa", "metadata_extraction_job": "Metaadatok kinyerÊse", "metadata_extraction_job_description": "Metaadat informÃĄciÃŗk (pl. GPS, arcok Ês felbontÃĄs) kinyerÊse minden elemből", "metadata_faces_import_setting": "Arc importÃĄlÃĄs engedÊlyezÊse", @@ -164,12 +166,18 @@ "metadata_settings_description": "Metaadat beÃĄllítÃĄsok kezelÊse", "migration_job": "MigrÃĄlÃĄs", "migration_job_description": "Az elemek Ês arcok bÊlyegkÊpeinek migrÃĄlÃĄsa a legÃējabb mappastruktÃērÃĄba", + "nightly_tasks_cluster_faces_setting_description": "ArcfelismerÊs futtatÃĄsa az Ãējonnan ÊrzÊkelt arcokon", + "nightly_tasks_database_cleanup_setting": "AdatbÃĄzis-tisztítÃĄsi feladatok", + "nightly_tasks_database_cleanup_setting_description": "A rÊgi, lejÃĄrt adatok tÃļrlÊse az adatbÃĄzisbÃŗl", + "nightly_tasks_generate_memories_setting": "EmlÊkek generÃĄlÃĄsa", + "nightly_tasks_generate_memories_setting_description": "Új emlÊkek lÊtrehozÃĄsa elemekből", + "nightly_tasks_missing_thumbnails_setting": "HiÃĄnyzÃŗ indexkÊpek generÃĄlÃĄsa", "no_paths_added": "Nincs megadva elÊrÊsi Ãētvonal", "no_pattern_added": "Nincs megadva minta (pattern)", "note_apply_storage_label_previous_assets": "MegjegyzÊs: Ha a korÃĄbban feltÃļltÃļtt elemekhez is szeretne TÃĄrhely CímkÊket tÃĄrsítani, akkor futtassa ezt", "note_cannot_be_changed_later": "FIGYELEM: ezt kÊsőbb nem lehet megvÃĄltoztatni!", "notification_email_from_address": "FeladÃŗ cím", - "notification_email_from_address_description": "KÃŧldő email címe, pÊldÃĄul: \"Immich FotÃŗszerver \"", + "notification_email_from_address_description": "KÃŧldő email címe, pÊldÃĄul: \"Immich FotÃŗszerver \". Figyelj hogy olyan címet adj meg ahonnan az email kÃŧldÊs engedÊlyezett.", "notification_email_host_description": "Email szerver kiszolgÃĄlÃŗja (pl. smtp.immich.app)", "notification_email_ignore_certificate_errors": "TanÃēsítvÃĄny hibÃĄk figyelmen kívÃŧl hagyÃĄsa", "notification_email_ignore_certificate_errors_description": "TLS tanÃēsítvÃĄny ÊrvÊnyessÊgi hibÃĄk figyelmen kívÃŧl hagyÃĄsa (nem ajÃĄnlott)", @@ -202,7 +210,7 @@ "oauth_storage_quota_claim": "TÃĄrhelykvÃŗta igÊnylÊse", "oauth_storage_quota_claim_description": "A felhasznÃĄlÃŗ tÃĄrhelykvÃŗtÃĄjÃĄnak automatikus beÃĄllítÃĄsa ennek az igÊnyeltre.", "oauth_storage_quota_default": "AlapÊrtelmezett tÃĄrhelykvÃŗta (GiB)", - "oauth_storage_quota_default_description": "AlapÊrtelmezett tÃĄrhely kvÃŗta GiB-ban, amennyiben a felhasznÃĄlÃŗ nem jelezte az igÊnyÊt (A korlÃĄtlan tÃĄrhelyhez 0-t adj meg).", + "oauth_storage_quota_default_description": "AlapÊrtelmezett tÃĄrhely kvÃŗta GiB-ban, amennyiben a felhasznÃĄlÃŗ nem jelezte az igÊnyÊt.", "oauth_timeout": "KÊrÊs időkorlÃĄtja", "oauth_timeout_description": "KÊrÊsek időkorlÃĄtja milliszekundumban", "password_enable_description": "BejelentkezÊs emaillel Ês jelszÃŗval", @@ -242,6 +250,7 @@ "storage_template_migration_info": "A sablon az Ãļsszes kiterjesztÊst kisbetÅąssÊ alakítja ÃĄt. A megvÃĄltozott sablon csak az Ãējonnan feltÃļltÃļtt elemekre vonatkozik. A korÃĄbbi elemek visszamenőleges ÃĄthelyezÊsÊhez ezt futtasd: {job}.", "storage_template_migration_job": "TÃĄrhely Sablon MigrÃĄciÃŗja", "storage_template_more_details": "TovÃĄbbi rÊszletekÊrt erről a funkciÃŗrÃŗl lÃĄsd a TÃĄrhely Sablon Ês annak kÃļvetkezmÊnyeit a dokumentÃĄciÃŗban", + "storage_template_onboarding_description_v2": "A funkciÃŗ engedÊlyezÊsÊvel automatikusan, a felhasznÃĄlÃŗ ÃĄltal definiÃĄlt sablon alapjÃĄn lesznek rendezve a fÃĄjlok. TÃļbb informÃĄciÃŗÃŠrt lÃĄsd a dokumentÃĄciÃŗt.", "storage_template_path_length": "Útvonal hozzÃĄvetőleges maximÃĄlis hossza: {length, number}{limit, number}", "storage_template_settings": "TÃĄrhely Sablon", "storage_template_settings_description": "A feltÃļltÃļtt elemek mappaszerkezetÊnek Ês fÃĄjl elnevezÊsÊnek kezelÊse", @@ -256,7 +265,7 @@ "template_email_update_album": "Album frissítve sablon", "template_email_welcome": "ÜdvÃļzlő email sablon", "template_settings": "ÉrtesítÊs sablon", - "template_settings_description": "EgyÊni sablonok kezelÊse az ÊrtesítÊsekhez.", + "template_settings_description": "EgyÊni sablonok kezelÊse az ÊrtesítÊsekhez", "theme_custom_css_settings": "Egyedi CSS", "theme_custom_css_settings_description": "CSS Stíluslapokkal az Immich stílusa megvÃĄltoztathatÃŗ.", "theme_settings": "TÊma BeÃĄllítÃĄsok", @@ -288,7 +297,7 @@ "transcoding_encoding_options": "EnkÃŗdolÃĄs beÃĄllítÃĄsok", "transcoding_encoding_options_description": "BeÃĄllíthatod az enkÃŗdolt videÃŗk kÃŗdolÃĄsi algoritmusÃĄt, felbontÃĄsÃĄt, minősÊgÊt Ês egyÊb beÃĄllítÃĄsait", "transcoding_hardware_acceleration": "Hardveres GyorsítÃĄs", - "transcoding_hardware_acceleration_description": "KísÊrleti funkciÃŗ. Sokkal gyorsabb, viszont azonos bitrÃĄtÃĄn is alacsonyabb minősÊghez vezet", + "transcoding_hardware_acceleration_description": "KísÊrleti funkciÃŗ: gyorsabb transzkÃŗdolÃĄs, viszont azonos bitrÃĄtÃĄn alacsonyabb minősÊghez vezethet", "transcoding_hardware_decoding": "Hardveres dekÃŗdolÃĄs", "transcoding_hardware_decoding_setting_description": "LehetővÊ teszi az egÊsz folyamat gyorsítÃĄsÃĄt a pusztÃĄn kÃŗdolÃĄs gyorsítÃĄsa helyett. Nem biztos, hogy minden videÃŗ esetÊn mÅąkÃļdik.", "transcoding_max_b_frames": "B-kÊpkockÃĄk maximum szÃĄma", @@ -334,6 +343,7 @@ "user_delete_delay_settings_description": "HÃĄny nappal az eltÃĄvolítÃĄs utÃĄn legyen vÊglegesen tÃļrÃļlve a felhasznÃĄlÃŗ fiÃŗkja Ês tÃĄrolt elemei. A vÊgleges tÃļrlÊs feladat minden ÊjfÊlkor fut le, hogy ellenőrizze, hogy van-e tÃļrlendő felhasznÃĄlÃŗ. Ez a beÃĄllítÃĄs a kÃļvetkező futtatÃĄs sorÃĄn lÊp Êletbe.", "user_delete_immediately": "{user} felhasznÃĄlÃŗja Ês Ãļsszes eleme azonnal sorba ÃĄllítÃĄsra kerÃŧl a vÊgleges tÃļrlÊshez .", "user_delete_immediately_checkbox": "FelhasznÃĄlÃŗ Ês tÃĄrolt elemeinek sorba ÃĄllítÃĄsa azonnali tÃļrlÊsre", + "user_details": "FelhasznÃĄlÃŗi adatok", "user_management": "FelhasznÃĄlÃŗk KezelÊse", "user_password_has_been_reset": "A felhasznÃĄlÃŗ jelszava megvÃĄltoztatÃĄsra kerÃŧlt:", "user_password_reset_description": "Juttasd el az ÃĄtmeneti jelszÃŗt a felhasznÃĄlÃŗhoz Ês tÃĄjÊkoztasd, hogy a kÃļvetkező belÊpÊsnÊl azt majd meg kell vÃĄltoztatnia.", @@ -349,19 +359,21 @@ "video_conversion_job": "VideÃŗk ÁtkÃŗdolÃĄsa", "video_conversion_job_description": "VideÃŗk ÃĄtkÃŗdolÃĄsa bÃļngÊszőkkel Ês eszkÃļzÃļkkel valÃŗ szÊleskÃļrÅą kompatibilitÃĄs ÊrdekÊben" }, + "admin_email": "Admin e-mail", "admin_password": "Admin JelszÃŗ", "administration": "AdminisztrÃĄciÃŗ", "advanced": "HaladÃŗ", "advanced_settings_enable_alternate_media_filter_subtitle": "Ezzel a beÃĄllítÃĄssal a szinkronizÃĄlÃĄs sorÃĄn alternatív kritÊriumok alapjÃĄn szÅąrheted a fÃĄjlokat. Csak akkor prÃŗbÃĄld ki, ha problÊmÃĄid vannak azzal, hogy az alkalmazÃĄs nem ismeri fel az Ãļsszes albumot.", "advanced_settings_enable_alternate_media_filter_title": "[KÍSÉRLETI] Alternatív eszkÃļz album szinkronizÃĄlÃĄsi szÅąrő hasznÃĄlata", "advanced_settings_log_level_title": "NaplÃŗzÃĄs szintje: {level}", - "advanced_settings_prefer_remote_subtitle": "NÊhÃĄny eszkÃļz fÃĄjdalmasan lassan tÃļlti be az eszkÃļzÃļn lÊvő bÊlyegkÊpeket. Ez a beÃĄllítÃĄs inkÃĄbb a tÃĄvoli kÊpeket tÃļlti be helyettÃŧk.", + "advanced_settings_prefer_remote_subtitle": "NÊhÃĄny eszkÃļz fÃĄjdalmasan lassan tÃļlti be az eszkÃļzÃļn lÊvő indexkÊpeket. Ez a beÃĄllítÃĄs inkÃĄbb a tÃĄvoli kÊpeket (a szerverről) tÃļlti be helyettÃŧk.", "advanced_settings_prefer_remote_title": "TÃĄvoli kÊpek előnyben rÊszesítÊse", "advanced_settings_proxy_headers_subtitle": "Add meg azokat a proxy fejlÊceket, amiket az app elkÃŧldjÃļn minden hÃĄlÃŗzati kÊrÊsnÊl", "advanced_settings_proxy_headers_title": "Proxy FejlÊcek", "advanced_settings_self_signed_ssl_subtitle": "Nem ellenőrzi a szerver SSL tanÃēsítvÃĄnyÃĄt. ÖnalÃĄÃ­rt tanÃēsítvÃĄny esetÊn szÃŧksÊges beÃĄllítÃĄs.", "advanced_settings_self_signed_ssl_title": "ÖnalÃĄÃ­rt SSL tanÃēsítvÃĄnyok engedÊlyezÊse", "advanced_settings_sync_remote_deletions_subtitle": "Automatikusan tÃļrÃļlni vagy visszaÃĄllítani egy elemet ezen az eszkÃļzÃļn, ha az adott mÅąveletet a weben hajtottÃĄk vÊgre", + "advanced_settings_sync_remote_deletions_title": "TÃĄvoli tÃļrlÊsek szinkronizÃĄlÃĄsa [KÍSÉRLETI FUNKCIÓ]", "advanced_settings_tile_subtitle": "HaladÃŗ felhasznÃĄlÃŗi beÃĄllítÃĄsok", "advanced_settings_troubleshooting_subtitle": "TovÃĄbbi funkciÃŗk engedÊlyezÊse hibaelhÃĄrítÃĄs cÊljÃĄbÃŗl", "advanced_settings_troubleshooting_title": "HibaelhÃĄrítÃĄs", @@ -398,6 +410,9 @@ "album_with_link_access": "A link birtokÃĄban bÃĄrki lÃĄthatja a fotÃŗkat Ês a szemÊlyeket ebben az albumban.", "albums": "Albumok", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Album}}", + "albums_default_sort_order": "AlapÊrtelmezett album rendezÊs", + "albums_default_sort_order_description": "AlapÊrtelmezett sorrendezÊs Ãēj albumok lÊtrehozÃĄsÃĄnÃĄl.", + "albums_feature_description": "MÃĄsokkal megoszthatÃŗ elemek gyÅąjtemÊnye.", "all": "Mind", "all_albums": "Minden album", "all_people": "Minden szemÊly", @@ -455,10 +470,11 @@ "assets": "Elemek", "assets_added_count": "{count, plural, other {# elem}} hozzÃĄadva", "assets_added_to_album_count": "{count, plural, other {# elem}} hozzÃĄadva az albumhoz", - "assets_added_to_name_count": "{count, plural, other {# elem}} hozzÃĄadva {hasName, select, true {a(z) {name}} other {az Ãēj}} albumhoz", + "assets_cannot_be_added_to_album_count": "{count, plural, one {Az elem} other {Az elemek}} nem adhatÃŗak hozzÃĄ az albumhoz", "assets_count": "{count, plural, other {# elem}}", "assets_deleted_permanently": "{count} elem vÊglegesen tÃļrÃļlve", "assets_deleted_permanently_from_server": "{count} elem vÊglegesen tÃļrÃļlve az Immich szerverről", + "assets_downloaded_successfully": "{count, plural, one {# fÃĄjl sikeresen letÃļltve} other {# fÃĄjl sikeresen letÃļltve}}", "assets_moved_to_trash_count": "{count, plural, other {# elem}} ÃĄthelyezve a lomtÃĄrba", "assets_permanently_deleted_count": "{count, plural, other {# elem}} vÊglegesen tÃļrÃļlve", "assets_removed_count": "{count, plural, other {# elem}} eltÃĄvolítva", @@ -484,10 +500,10 @@ "backup_album_selection_page_selection_info": "ÖsszegzÊs", "backup_album_selection_page_total_assets": "Összes egyedi elem", "backup_all": "Összes", - "backup_background_service_backup_failed_message": "Az elemek mentÊse sikertelen. ÚjraprÃŗbÃĄlkozÃĄs...", - "backup_background_service_connection_failed_message": "A szerverhez csatlakozÃĄs sikertelen. ÚjraprÃŗbÃĄlkozÃĄs...", + "backup_background_service_backup_failed_message": "Az elemek mentÊse sikertelen. ÚjraprÃŗbÃĄlkozÃĄsâ€Ļ", + "backup_background_service_connection_failed_message": "A szerverhez csatlakozÃĄs sikertelen. ÚjraprÃŗbÃĄlkozÃĄsâ€Ļ", "backup_background_service_current_upload_notification": "FeltÃļltÊs {filename}", - "backup_background_service_default_notification": "Új elemek ellenőrzÊse...", + "backup_background_service_default_notification": "Új elemek ellenőrzÊseâ€Ļ", "backup_background_service_error_title": "Hiba a mentÊs kÃļzben", "backup_background_service_in_progress_notification": "Elemek mentÊse folyamatbanâ€Ļ", "backup_background_service_upload_failure_notification": "A feltÃļltÊs sikertelen {filename}", @@ -497,6 +513,7 @@ "backup_controller_page_background_app_refresh_enable_button_text": "BeÃĄllítÃĄsok megnyitÃĄsa", "backup_controller_page_background_battery_info_link": "Mutasd meg hogyan", "backup_controller_page_background_battery_info_message": "A sikeres hÃĄttÊrben tÃļrtÊnő mentÊshez kÊrjÃŧk, tiltsd le az Immich akkumulÃĄtor optimalizÃĄlÃĄsÃĄt.\n\nMivel ezt a kÃŧlÃļnfÊle eszkÃļzÃļkÃļn mÃĄshogy kell, ezÊrt kÊrjÃŧk, az eszkÃļzÃļd gyÃĄrtÃŗjÃĄtÃŗl tudd meg, hogyan kell.", + "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "AkkumulÃĄtor optimalizÃĄlÃĄs", "backup_controller_page_background_charging": "Csak tÃļltÊs kÃļzben", "backup_controller_page_background_configure_error": "A hÃĄttÊrszolgÃĄltatÃĄs beÃĄllítÃĄsa sikertelen", @@ -506,7 +523,7 @@ "backup_controller_page_background_is_on": "Automatikus mentÊs a hÃĄttÊrben be van kapcsolva", "backup_controller_page_background_turn_off": "HÃĄttÊrszolgÃĄltatÃĄs kikapcsolÃĄsa", "backup_controller_page_background_turn_on": "HÃĄttÊrszolgÃĄltatÃĄs bekapcsolÃĄsa", - "backup_controller_page_background_wifi": "Csak WiFi-n", + "backup_controller_page_background_wifi": "Csak Wi-Fi-n", "backup_controller_page_backup": "MentÊs", "backup_controller_page_backup_selected": "KivÃĄlasztva: ", "backup_controller_page_backup_sub": "Mentett fotÃŗk Ês videÃŗk", @@ -539,6 +556,10 @@ "backup_options_page_title": "BiztonÃĄgi mentÊs beÃĄllítÃĄsai", "backup_setting_subtitle": "A hÃĄttÊrben Ês előtÊrben mentÊs beÃĄllítÃĄsainak kezelÊse", "backward": "Visszafele", + "biometric_auth_enabled": "Biometrikus azonosítÃĄs engedÊlyezve", + "biometric_locked_out": "Ki vagy zÃĄrva a biometrikus azonosítÃĄsbÃŗl", + "biometric_no_options": "Nincsen elÊrhető biometrikus azonosítÃĄs", + "biometric_not_available": "Biometrikus azonosítÃĄs ezen az eszkÃļzÃļn nem elÊrhető", "birthdate_saved": "SzÃŧletÊsnap elmentve", "birthdate_set_description": "A szÃŧletÊs napjÃĄt a rendszer arra hasznÃĄlja, hogy kiírja, hogy a fÊnykÊp kÊszítÊsekor a szemÊly hÃĄny Êves volt.", "blurred_background": "HomÃĄlyos hÃĄttÊr", @@ -572,6 +593,7 @@ "cannot_undo_this_action": "Ez a mÅąvelet nem visszavonhatÃŗ!", "cannot_update_the_description": "A leírÃĄs megvÃĄltoztatÃĄsa nem sikerÃŧlt", "change_date": "DÃĄtum vÃĄltoztatÃĄsa", + "change_description": "LeírÃĄs megvÃĄltoztatÃĄsa", "change_display_order": "MegjelenítÊsi sorrend megvÃĄltoztatÃĄsa", "change_expiration_time": "LejÃĄrati idő megvÃĄltoztatÃĄsa", "change_location": "Helyszín vÃĄltoztatÃĄsa", @@ -598,6 +620,7 @@ "clear_all_recent_searches": "LegutÃŗbbi keresÊsek tÃļrlÊse", "clear_message": "Üzenet tÃļrlÊse", "clear_value": "ÉrtÊk tÃļrlÊse", + "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "JelszÃŗ MegadÃĄsa", "client_cert_import": "ImportÃĄlÃĄs", "client_cert_import_success_msg": "Kliens tanÃēsítvÃĄny importÃĄlva", @@ -625,6 +648,10 @@ "confirm_keep_this_delete_others": "Minden mÃĄs elem a kÊszletben tÃļrlÊsre kerÃŧl, kivÊve ezt az elemet. Biztosan folytatni szeretnÊd?", "confirm_new_pin_code": "Új PIN kÃŗd megerősítÊse", "confirm_password": "JelszÃŗ megerősítÊse", + "confirm_tag_face": "SzeretnÊd ezt az arcot {name}-nak/nek megjelÃļlni?", + "confirm_tag_face_unnamed": "SzeretnÊd ezt az arcot megjelÃļlni?", + "connected_device": "Kapcsolt eszkÃļz", + "connected_to": "KapcsolÃŗdva", "contain": "BelÃŧl", "context": "Kontextus", "continue": "FolytatÃĄs", @@ -633,6 +660,7 @@ "control_bottom_app_bar_delete_from_local": "TÃļrlÊs az eszkÃļzről", "control_bottom_app_bar_edit_location": "Hely MÃŗdosítÃĄsa", "control_bottom_app_bar_edit_time": "DÃĄtum Ês Idő MÃŗdosítÃĄsa", + "control_bottom_app_bar_share_link": "Link megosztÃĄsa", "control_bottom_app_bar_share_to": "MegosztÃĄs Ide", "control_bottom_app_bar_trash_from_immich": "LomtÃĄrba Helyez", "copied_image_to_clipboard": "KÊp a vÃĄgÃŗlapra mÃĄsolva.", @@ -664,6 +692,7 @@ "create_tag_description": "Új címke lÊtrehozÃĄsa. BeÃĄgyazott címkÊk esetÊn add meg a címke teljes elÊrÊsi ÃētvonalÃĄt, beleÊrtve a perjeleket is.", "create_user": "FelhasznÃĄlÃŗ lÊtrehozÃĄsa", "created": "KÊszÃŧlt", + "created_at": "LÊtrehozva", "crop": "KivÃĄgÃĄs", "curated_object_page_title": "Dolgok", "current_device": "Ez az eszkÃļz", @@ -719,7 +748,9 @@ "direction": "IrÃĄny", "disabled": "Letiltott", "disallow_edits": "MÃŗdosítÃĄsok letiltÃĄsa", + "discord": "Discord", "discover": "Felfedez", + "discovered_devices": "Felfedezett eszkÃļzÃļk", "dismiss_all_errors": "Minden hiba elvetÊse", "dismiss_error": "Hiba elvetÊse", "display_options": "MegjelenítÊsi beÃĄllítÃĄsok", @@ -744,8 +775,8 @@ "download_settings_description": "Elemek letÃļltÊsÊvel kapcsolatos beÃĄllítÃĄsok kezelÊse", "download_started": "LetÃļltÊs megkezdve", "download_sucess": "Sikeres letÃļltÊs", - "download_sucess_android": "MÊdia letÃļltve a DCIM/Immich mappÃĄba\n", - "download_waiting_to_retry": "VÃĄrakozÃĄs", + "download_sucess_android": "MÊdia letÃļltve a DCIM/Immich mappÃĄba", + "download_waiting_to_retry": "VÃĄrÃĄs az ÃējraprÃŗbÃĄlkozÃĄsra", "downloading": "LetÃļltÊs", "downloading_asset_filename": "{filename} elem letÃļltÊse", "downloading_media": "MÊdia letÃļltÊse", @@ -758,6 +789,8 @@ "edit_avatar": "ProfilkÊp mÃŗdosítÃĄsa", "edit_date": "DÃĄtum mÃŗdosítÃĄsa", "edit_date_and_time": "DÃĄtum Ês idő mÃŗdosítÃĄsa", + "edit_description": "LeírÃĄs szerkesztÊse", + "edit_description_prompt": "KÊrlek vÃĄlassz egy Ãēj leírÃĄst:", "edit_exclusion_pattern": "KizÃĄrÃĄsi minta (pattern) mÃŗdosítÃĄsa", "edit_faces": "Arcok mÃŗdosítÃĄsa", "edit_import_path": "ImportÃĄlÃĄsi Ãētvonal mÃŗdosítÃĄsa", @@ -777,18 +810,25 @@ "editor_close_without_save_title": "Szerkesztő bezÃĄrÃĄsa?", "editor_crop_tool_h2_aspect_ratios": "OldalarÃĄnyok", "editor_crop_tool_h2_rotation": "ForgatÃĄs", + "email": "E-mail", + "email_notifications": "E-mail ÊrtesítÊsek", + "empty_folder": "Ez a mappa Ãŧres", "empty_trash": "LomtÃĄr ÃŧrítÊse", "empty_trash_confirmation": "Biztosan kiÃŧríted a lomtÃĄrat? Ez az Immich lomtÃĄrÃĄban lÊvő Ãļsszes elemet vÊglegesen tÃļrli.\nEz a mÅąvelet nem visszavonhatÃŗ!", "enable": "EngedÊlyezÊs", + "enable_biometric_auth_description": "Add meg a jelszavad a biometrikus azonosítÃĄs engedÊlyezÊsÊhez", "enabled": "EngedÊlyezve", "end_date": "VÊg dÃĄtum", "enqueued": "Sorba ÃĄllítva", - "enter_wifi_name": "Add meg a WiFi hÃĄlÃŗzat nevÊt", + "enter_wifi_name": "Add meg a Wi-Fi hÃĄlÃŗzat nevÊt", + "enter_your_pin_code": "Add meg a jelszavad", + "enter_your_pin_code_subtitle": "Add meg a PIN kÃŗdodat a zÃĄrolt mappa megnyitÃĄsÃĄhoz", "error": "Hiba", "error_change_sort_album": "Album sorbarendezÊsÊnek megvÃĄltoztatÃĄsa sikertelen", "error_delete_face": "Hiba az arc tÃļrlÊse sorÃĄn", "error_loading_image": "Hiba a kÊp betÃļltÊse kÃļzben", "error_saving_image": "Hiba: {error}", + "error_tag_face_bounding_box": "Hiba az arc megjelÃļlÊse kÃļzben - nem elÊrhetőek a hatÃĄrolÃŗ koordinÃĄtÃĄk", "error_title": "Hiba - valami fÊlresikerÃŧlt", "errors": { "cannot_navigate_next_asset": "Nem lehet a kÃļvetkező elemhez navigÃĄlni", @@ -816,10 +856,12 @@ "failed_to_keep_this_delete_others": "Nem sikerÃŧlt megtartani ezt az elemet, Ês a tÃļbbi elemet tÃļrÃļlni", "failed_to_load_asset": "Elem betÃļltÊse sikertelen", "failed_to_load_assets": "Elemek betÃļltÊse sikertelen", + "failed_to_load_notifications": "ÉrtesítÊsek betÃļltÊse sikertelen", "failed_to_load_people": "SzemÊlyek betÃļltÊse sikertelen", "failed_to_remove_product_key": "TermÊkkulcs eltÃĄvolítÃĄsa sikertelen", "failed_to_stack_assets": "Elemek csoportosítÃĄsa sikertelen", "failed_to_unstack_assets": "Csoportosított elemek szÊtszedÊse sikertelen", + "failed_to_update_notification_status": "ÉrtesítÊs stÃĄtusz frissítÊse sikertelen", "import_path_already_exists": "Ez az importÃĄlÃĄsi Ãētvonal mÃĄr lÊtezik.", "incorrect_email_or_password": "Helytelen email vagy jelszÃŗ", "paths_validation_failed": "A(z) {paths, plural, one {# elÊrÊsi Ãētvonal} other {# elÊrÊsi Ãētvonal}} ÊrvÊnyesítÊse sikertelen", @@ -836,6 +878,7 @@ "unable_to_archive_unarchive": "Az elem {archived, select, true {archivÃĄlÃĄsa} other {kivÊtele az archívumbÃŗl}} sikertelen", "unable_to_change_album_user_role": "Az album felhasznÃĄlÃŗi jogkÃļrÊnek megvÃĄltoztatÃĄsa sikertelen", "unable_to_change_date": "DÃĄtum megvÃĄltoztatÃĄsa sikertelen", + "unable_to_change_description": "LeírÃĄs mÃŗdosítÃĄsa sikertelen", "unable_to_change_favorite": "Az elem kedvenc ÃĄllapotÃĄnak megvÃĄltoztatÃĄsa sikertelen", "unable_to_change_location": "Hely megvÃĄltoztatÃĄsa sikertelen", "unable_to_change_password": "JelszÃŗ megvÃĄltoztatÃĄsa sikertelen", @@ -879,6 +922,7 @@ "unable_to_remove_partner": "Partner eltÃĄvolítÃĄsa sikertelen", "unable_to_remove_reaction": "ReakciÃŗ eltÃĄvolítÃĄsa sikertelen", "unable_to_reset_password": "JelszÃŗ visszaÃĄllítÃĄsa sikertelen", + "unable_to_reset_pin_code": "PIN kÃŗd visszaÃĄllítÃĄsa sikertelen", "unable_to_resolve_duplicate": "DuplikÃĄtum feloldÃĄsa sikertelen", "unable_to_restore_assets": "Elemek visszaÃĄllítÃĄsa sikertelen", "unable_to_restore_trash": "Az Ãļsszes elem visszaÃĄllítÃĄsa sikertelen", @@ -906,11 +950,14 @@ "unable_to_update_user": "FelhasznÃĄlÃŗ mÃŗdosítÃĄsa sikertelen", "unable_to_upload_file": "FÃĄjlfeltÃļltÊs sikertelen" }, + "exif": "Exif", "exif_bottom_sheet_description": "LeírÃĄs HozzÃĄadÃĄsa...", "exif_bottom_sheet_details": "RÉSZLETEK", "exif_bottom_sheet_location": "HELY", "exif_bottom_sheet_people": "EMBEREK", "exif_bottom_sheet_person_add_person": "Elnevez", + "exif_bottom_sheet_person_age_months": "{months} hÃŗnap idős", + "exif_bottom_sheet_person_age_year_months": "1 Êv, {months} hÃŗnap idős", "exit_slideshow": "KilÊpÊs a DiavetítÊsből", "expand_all": "Összes kinyitÃĄsa", "experimental_settings_new_asset_list_subtitle": "FejlesztÊs alatt", @@ -928,10 +975,12 @@ "external": "KÃŧlső KÊptÃĄr", "external_libraries": "KÃŧlső KÊptÃĄrak", "external_network": "KÃŧlső hÃĄlÃŗzat", - "external_network_sheet_info": "Ha nem vagy a megadott WiFi hÃĄlÃŗzathoz csatlakozva, akkor az alkalmazÃĄs az alÃĄbbi URL címeken fogja elÊrni a szervert, fentről lefelÊ haladva", + "external_network_sheet_info": "Ha nem vagy a megadott Wi-Fi hÃĄlÃŗzathoz csatlakozva, akkor az alkalmazÃĄs az alÃĄbbi URL címeken fogja elÊrni a szervert, fentről lefelÊ haladva", "face_unassigned": "Nincs hozzÃĄrendelve", "failed": "Sikertelen", + "failed_to_authenticate": "AutentikÃĄciÃŗ sikertelen", "failed_to_load_assets": "Nem sikerÃŧlt betÃļlteni az elemeket", + "failed_to_load_folder": "Mappa betÃļltÊse sikertelen", "favorite": "Kedvenc", "favorite_or_unfavorite_photo": "FotÃŗ kedvencnek jelÃļlÊse vagy annak visszavonÃĄsa", "favorites": "Kedvencek", @@ -945,14 +994,19 @@ "filetype": "FÃĄjltípus", "filter": "SzÅąrő", "filter_people": "SzemÊlyek szÅąrÊse", + "filter_places": "Helyszínek szÅąrÊse", "find_them_fast": "NÊv alapjÃĄn keresÊssel gyorsan megtalÃĄlhatÃŗak", "fix_incorrect_match": "HibÃĄs talÃĄlat javítÃĄsa", + "folder": "Mappa", + "folder_not_found": "Mappa nem talÃĄlhatÃŗ", "folders": "MappÃĄk", "folders_feature_description": "A fÃĄjlrendszerben lÊvő fÊnykÊpek Ês videÃŗk mappanÊzetben valÃŗ bÃļngÊszÊse", "forward": "Előre", + "gcast_enabled": "Google Cast", + "gcast_enabled_description": "Ez a funkciÃŗ a Google-től tÃļlti be a mÅąkÃļdÊsÊhez szÃŧksÊges kÃŧlső adatokat.", "general": "ÁltalÃĄnos", "get_help": "SegítsÊgkÊrÊs", - "get_wifiname_error": "Nem sikerÃŧlt lekÊrni a Wi-Fi nevÊt. Győződj meg rÃŗla, hogy megadtad a szÃŧksÊges engedÊlyeket Ês csatlakoztÃĄl egy Wi-Fi hÃĄlÃŗzathoz.", + "get_wifiname_error": "Nem sikerÃŧlt lekÊrni a Wi-Fi nevÊt. Győződj meg rÃŗla, hogy megadtad a szÃŧksÊges engedÊlyeket Ês csatlakoztÃĄl egy Wi-Fi hÃĄlÃŗzathoz", "getting_started": "Kezdő LÊpÊsek", "go_back": "VisszalÊpÊs", "go_to_folder": "UgrÃĄs a mappÃĄhoz", @@ -981,9 +1035,9 @@ "hide_person": "SzemÊly elrejtÊse", "hide_unnamed_people": "NÊv nÊlkÃŧli szemÊlyek elrejtÊse", "home_page_add_to_album_conflicts": "{added} elem hozzÃĄadva a(z) \"{album}\" albumhoz. {failed} elem mÃĄr eleve az albumban volt.", - "home_page_add_to_album_err_local": "Helyi elemeket mÊg nem lehet albumba tenni. Kihagyjuk.", + "home_page_add_to_album_err_local": "Helyi elemeket mÊg nem lehet albumba tenni, ki lesznek hagyva", "home_page_add_to_album_success": "{added} elem hozzÃĄadva a(z) \"{album}\" albumhoz.", - "home_page_album_err_partner": "MÊg nem lehet a partner elemeit albumokhoz adni, Ãēghogy kihagyjuk.", + "home_page_album_err_partner": "MÊg nem lehet a partner elemeit albumokhoz adni, ki lesznek hagyva", "home_page_archive_err_local": "Helyi elemek archivÃĄlÃĄsa mÊg nem tÃĄmogatott, Ãēgyhogy kihagyjuk", "home_page_archive_err_partner": "Partner elemeit nem lehet archivÃĄlni, Ãēgyhogy kihagyjuk", "home_page_building_timeline": "Idővonal ÃļsszeÃĄllítÃĄsa", @@ -991,11 +1045,14 @@ "home_page_delete_remote_err_local": "Helyi elemek vannak tÃĄvoli tÃļrlÊsre kivÃĄlasztva, Ãēgyhogy ezeket kihagyjuk", "home_page_favorite_err_local": "Helyi elemeket mÊg nem lehet a kedvencek kÃļzÊ tenni, Ãēgyhogy ezeket kihagyjuk", "home_page_favorite_err_partner": "Partner elemeit mÊg nem lehet a kedvencek kÃļzÊ tenni, Ãēgyhogy ezeket kihagyjuk", - "home_page_first_time_notice": "Ha most hasznÃĄlod előszÃļr az alkalmazÃĄst, akkor ahhoz, hogy megjelenjenek a fotÃŗk Ês a videÃŗk az idővonaladon, ÃĄllítsd be, hogy melyik albumaidrÃŗl kÊszÃŧljÃļn biztonsÃĄgi mentÊs.", + "home_page_first_time_notice": "Ha most hasznÃĄlod előszÃļr az alkalmazÃĄst, a fotÃŗk Ês videÃŗk megjelenítÊsÊhez az idővonaladon, ÃĄllítsd be, hogy melyik albumaidrÃŗl kÊszÃŧljÃļn biztonsÃĄgi mentÊs", + "home_page_locked_error_local": "A Helyi elemek nem mozgathatÃŗak a zÃĄrolt mappÃĄba, ki lesznek hagyva", + "home_page_locked_error_partner": "Partner elemek nem mozgathatÃŗak a zÃĄrolt mappÃĄba, ÃĄtugorva", "home_page_share_err_local": "Helyi elemekről nem lehet megosztott linket kÊszíteni, Ãēgyhogy kihagyjuk", "home_page_upload_err_limit": "Csak 30 elemet tudsz egyszerre feltÃļlteni, Ãēgyhogy kihagyjuk", "host": "KiszolgÃĄlÃŗ", "hour": "Óra", + "id": "AzonosítÃŗ", "ignore_icloud_photos": "iCloud fotÃŗk figyelmen kívÃŧl hagyÃĄsa", "ignore_icloud_photos_description": "Az iCloud-ban tÃĄrolt fotÃŗk nem lesznek feltÃļltve az Immich szerverre", "image": "KÊp", @@ -1035,6 +1092,11 @@ "invalid_date_format": "ÉrvÊnytelen dÃĄtumformÃĄtum", "invite_people": "SzemÊlyek MeghívÃĄsa", "invite_to_album": "MeghívÃĄs az albumba", + "ios_debug_info_last_sync_at": "UtoljÃĄra szinkronizÃĄlva {dateTime}", + "ios_debug_info_no_processes_queued": "Nincs a sorban hÃĄttÊrfolyamat jelenleg", + "ios_debug_info_no_sync_yet": "MÊg nem futott szinkronizÃĄlÃŗ hÃĄttÊrfolyamat", + "ios_debug_info_processes_queued": "{count, plural, one {{count} hÃĄttÊrfolyamat előkÊszítve} other {{count} hÃĄttÊrfolyamat előkÊszítve}}", + "ios_debug_info_processing_ran_at": "A feldolgozÃĄs ekkor futott: {dateTime}", "items_count": "{count, plural, other {# elem}}", "jobs": "Feladatok", "keep": "Megtart", @@ -1043,6 +1105,8 @@ "kept_this_deleted_others": "Ez az elem Ês a tÃļrÃļltek meg lettek hagyva {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "BillentyÅąparancsok", "language": "Nyelv", + "language_no_results_subtitle": "PrÃŗbÃĄld mÃŗdosítani a szavaidat a keresÊsnÊl", + "language_search_hint": "Nyelvek keresÊse...", "language_setting_description": "VÃĄlaszd ki preferÃĄlt nyelvet", "last_seen": "UtoljÃĄra lÃĄttuk", "latest_version": "Legfrissebb VerziÃŗ", @@ -1071,14 +1135,17 @@ "local_network": "Helyi hÃĄlÃŗzat", "local_network_sheet_info": "Az alkalmazÊs ezen az URL címen fogja elÊrni a szervert, ha a megadott WiFi hÃĄlÃŗzathoz van csatlankozva", "location_permission": "HelymeghatÃĄrozÃĄsi engedÊly", - "location_permission_content": "HÃĄlÃŗzatok automatikus vÃĄltÃĄsÃĄhoz az Immich-nek *mindenkÊppen* hozzÃĄ kell fÊrnie a pontos helyzethez, hogy az alkalmazÃĄs le tudja kÊrni a Wi-Fi hÃĄlÃŗzat nevÊt", + "location_permission_content": "A HÃĄlÃŗzatok automatikus vÃĄltÃĄsÃĄhoz az Immich-nek szÃŧksÊge van a pontos helymeghatÃĄrozÃĄsra, hogy az alkalmazÃĄs le tudja kÊrni a Wi-Fi hÃĄlÃŗzat nevÊt", "location_picker_choose_on_map": "VÃĄlassz a tÊrkÊpen", "location_picker_latitude_error": "ÉrvÊnyes szÊlessÊgi kÃļrt írj be", "location_picker_latitude_hint": "Ide írd a szÊlessÊgi kÃļrt", "location_picker_longitude_error": "ÉrvÊnyes hosszÃēsÃĄgi kÃļrt írj be", "location_picker_longitude_hint": "Ide írd a hosszÃēsÃĄgi kÃļrt", + "lock": "ZÃĄrolÃĄs", + "locked_folder": "ZÃĄrolt mappa", "log_out": "KijelentkezÊs", "log_out_all_devices": "KijelentkezÊs Minden EszkÃļzÃļn", + "logged_in_as": "BelÊpve: {user} nÊven", "logged_out_all_devices": "Minden eszkÃļz kijelentkeztetve", "logged_out_device": "EszkÃļz kijelentkeztetve", "login": "BejelentkezÊs", @@ -1093,7 +1160,7 @@ "login_form_err_invalid_url": "ÉrvÊnytelen cím", "login_form_err_leading_whitespace": "Az első karakter szÃŗkÃļz", "login_form_err_trailing_whitespace": "Az utolsÃŗ karakter szÃŗkÃļz", - "login_form_failed_get_oauth_server_config": "Nem sikerÃŧlt az OAuth bejelentkezÊs. Ellenőrizd a szerver címÊt.", + "login_form_failed_get_oauth_server_config": "Nem sikerÃŧlt az OAuth bejelentkezÊs. Ellenőrizd a szerver URL-t", "login_form_failed_get_oauth_server_disable": "OAuth bejelentkezÊs nem elÊrhető ezen a szerveren", "login_form_failed_login": "Hiba a bejelentkezÊs kÃļzben, ellenőrizd a szerver címÊt, az emailt Ês a jelszÃŗt", "login_form_handshake_exception": "SSL KÊzfogÃĄsi Hiba tÃļrÊnt. EngedÊlyezd az ÃļnalÃĄÃ­rt tanÃēsítvÊnyokat a beÃĄllítÃĄsokban, hogy ha ÃļnalÃĄÃ­rt tanÃēsítvÃĄnyt hasznÃĄlsz.", @@ -1145,6 +1212,9 @@ "map_settings_only_show_favorites": "Csak Kedvencek MutatÃĄsa", "map_settings_theme_settings": "TÊrkÊp TÊmÃĄja", "map_zoom_to_see_photos": "Kicsinyítsd, hogy lÃĄss fÊnykÊpeket", + "mark_all_as_read": "Összes megjelÃļlÊse olvasottkÊnt", + "mark_as_read": "MegjelÃļlÊs olvasottkÊnt", + "marked_all_as_read": "Összes megjelÃļlve olvasottkÊnt", "matches": "Azonosak", "media_type": "MÊdiatípus", "memories": "EmlÊkek", @@ -1169,6 +1239,12 @@ "month": "HÃŗnap", "monthly_title_text_date_format": "y MMMM", "more": "TovÃĄbbiak", + "move": "ÁthelyezÊs", + "move_off_locked_folder": "ÁtmozgatÃĄs a zÃĄrolt mappÃĄbÃŗl", + "move_to_locked_folder": "ÁthelyezÊs a zÃĄrolt mappÃĄba", + "move_to_locked_folder_confirmation": "Ezek a kÊpek Ês videÃŗk az Ãļsszes albumbÃŗl kikerÃŧlnek, Ês csak a zÃĄrolt mappÃĄban lesznek elÊrhetőek", + "moved_to_archive": "{count, plural, one {# Elem} other {# Elemek}} archivÃĄlva", + "moved_to_library": "{count, plural, one {# Elem} other {# Elemek}} mÃĄsik kÃļnyvtÃĄrba kÃļltÃļztetve", "moved_to_trash": "Áthelyezve a lomtÃĄrba", "multiselect_grid_edit_date_time_err_read_only": "Csak-olvashatÃŗ elem(ek) dÃĄtuma nem mÃŗdosíthatÃŗ, ezÊrt kihagyjuk", "multiselect_grid_edit_gps_err_read_only": "Csak-olvashatÃŗ elem(ek) helye nem mÃŗdosíthatÃŗ, ezÊrt kihagyjuk", @@ -1184,6 +1260,7 @@ "new_password": "Új jelszÃŗ", "new_person": "Új szemÊly", "new_pin_code": "Új PIN kÃŗd", + "new_pin_code_subtitle": "Ez az első alkalom hogy megnyitod a zÃĄrolt mappÃĄt. Hozz lÊtre egy jelszÃŗt a mappa biztonsÃĄgos elÊrÊsÊhez", "new_user_created": "Új felhasznÃĄlÃŗ lÊtrehozva", "new_version_available": "ÚJ VERZIÓ ÉRHETŐ EL", "newest_first": "LegÃējabb előszÃļr", @@ -1201,7 +1278,9 @@ "no_explore_results_message": "TÃļlts fel tÃļbb kÊpet, hogy bÃļngÊszhesd a gyÅąjtemÊnyed.", "no_favorites_message": "Add hozzÃĄ a kedvencekhez, hogy gyorsan megtalÃĄld a legjobb kÊpeidet Ês videÃŗidat", "no_libraries_message": "Hozz lÊtre kÃŧlső kÊptÃĄrat a fÊnykÊpeid Ês videÃŗid megtekintÊsÊhez", + "no_locked_photos_message": "A zÃĄrolt mappÃĄban elhelyezett fotÃŗk Ês videÃŗk rejtettek, Ês nem jelennek meg a kÃļnyvtÃĄrad bÃļngÊszÊse vagy keresÊse kÃļzben sem.", "no_name": "Nincs NÊv", + "no_notifications": "Nincsenek ÊrtesítÊsek", "no_places": "Nincsenek helyek", "no_results": "Nincs talÃĄlat", "no_results_description": "PrÃŗbÃĄlkozz szinonimÃĄkkal vagy ÃĄltalÃĄnosabb kulcsszavakkal", @@ -1210,6 +1289,7 @@ "not_selected": "Nincs kivÃĄlasztva", "note_apply_storage_label_to_previously_uploaded assets": "MegjegyzÊs: a korÃĄbban feltÃļltÃļtt elemek TÃĄrhely CímkÊzÊsÊhez futtasd a(z)", "notes": "MegjegyzÊsek", + "nothing_here_yet": "MÊg semmi sincs itt", "notification_permission_dialog_content": "Az ÊrtesítÊsek bekapcsolÃĄsÃĄhoz a BeÃĄllítÃĄsok menÃŧben vÃĄlaszd ki az EngedÊlyezÊs-t.", "notification_permission_list_tile_content": "ÉrtesítÊsek engedÊlyezÊse.", "notification_permission_list_tile_enable_button": "ÉrtesítÊsek BekapcsolÃĄsa", @@ -1217,15 +1297,21 @@ "notification_toggle_setting_description": "Email ÊrtesítÊsek engedÊlyezÊse", "notifications": "ÉrtesítÊsek", "notifications_setting_description": "ÉrtesítÊsek kezelÊse", + "oauth": "OAuth", "official_immich_resources": "Hivatalos Immich ForrÃĄsok", + "offline": "Nem elÊrhető (offline)", "ok": "Rendben", "oldest_first": "LegrÊgebbi előszÃļr", "on_this_device": "Ezen az eszkÃļzÃļn", "onboarding": "Első lÊpÊsek", - "onboarding_privacy_description": "Az alÃĄbbi (nem kÃļtelező) funkciÃŗk kÃŧlsős szolgÃĄltatÃĄsokon alapulnak Ês bÃĄrmikor kikapcsolhatÃŗak az adminisztrÃĄciÃŗs beÃĄllítÃĄsokban.", + "onboarding_locale_description": "VÃĄlaszd ki a preferÃĄlt nyelved. Ezt kÊsőbb a beÃĄllítÃĄsokban bÃĄrmikor mÃŗdosíthatod.", + "onboarding_privacy_description": "Az alÃĄbbi (nem kÃļtelező) funkciÃŗk kÃŧlsős szolgÃĄltatÃĄsokon alapulnak Ês bÃĄrmikor kikapcsolhatÃŗak a beÃĄllítÃĄsokban.", "onboarding_theme_description": "VÃĄlassz egy színtÊmÃĄt. Ezt bÃĄrmikor megvÃĄltoztathatod a beÃĄllítÃĄsokban.", + "onboarding_user_welcome_description": "KezdjÃŧnk bele!", "onboarding_welcome_user": "ÜdvÃļzÃļllek {user}", + "online": "Online (elÊrhető)", "only_favorites": "Csak kedvencek", + "open": "Nyitva", "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", @@ -1238,6 +1324,7 @@ "other_variables": "EgyÊb vÃĄltozÃŗk", "owned": "Tulajdonos", "owner": "Tulajdonos", + "partner": "Partner", "partner_can_access": "{partner} hozzÃĄfÊrhet", "partner_can_access_assets": "Minden fÊnykÊped Ês videÃŗd, kivÊve az ArchivÃĄltak Ês a TÃļrÃļltek", "partner_can_access_location": "A helyszín, ahol a fotÃŗkat kÊszítettÊk", @@ -1247,7 +1334,7 @@ "partner_page_no_more_users": "Nincs tÃļbb hozzÃĄadhatÃŗ felhasznÃĄlÃŗ", "partner_page_partner_add_failed": "Partner hozzÃĄadÃĄsa sikertelen", "partner_page_select_partner": "Partner kivÃĄlasztÃĄsa", - "partner_page_shared_to_title": "Megosztva: ", + "partner_page_shared_to_title": "Megosztva", "partner_page_stop_sharing_content": "{partner} nem fog tÃļbbÊ hozzÃĄfÊrni a fotÃŗidhoz.", "partner_sharing": "Partner MegosztÃĄs", "partners": "Partnerek", @@ -1277,6 +1364,8 @@ "permanently_delete_assets_prompt": "Biztos, hogy vÊglegesen tÃļrÃļlni {count, plural, one {szeretnÊd ezt az elemet} other {szeretnÊl # elemet}}? Ez el fogja tÃĄvolítani az {count, plural, one {elemet az albumokbÃŗl, amikben szerepel} other {elemeket az albumokbÃŗl, amikben szerepelnek}}.", "permanently_deleted_asset": "Elem vÊglegesen tÃļrÃļlve", "permanently_deleted_assets_count": "{count, plural, other {# elem}} vÊglegesen tÃļrÃļlve", + "permission": "JogosultsÃĄg", + "permission_empty": "A jogosultsÃĄg nem hagyhatÃŗ Ãŧresen", "permission_onboarding_back": "Vissza", "permission_onboarding_continue_anyway": "FolytatÃĄs mindenkÊpp", "permission_onboarding_get_started": "VÃĄgjunk bele", @@ -1284,7 +1373,7 @@ "permission_onboarding_permission_denied": "HozzÃĄfÊrÊs megtagadva. Az Immich hasznÃĄlatÃĄhoz engedÊlyezni kell a fotÃŗ Ês videÃŗ hozzÃĄfÊrÊst a BeÃĄllítÃĄsokban.", "permission_onboarding_permission_granted": "HozzÃĄfÊrÊs engedÊlyezve! Minden kÊszen ÃĄll.", "permission_onboarding_permission_limited": "KorlÃĄtozott hozzÃĄfÊrÊs. Ha szeretnÊd, hogy az Immich a teljes galÊria gyÅąjtemÊnyedet mentse Ês kezelje, akkor a BeÃĄllítÃĄsokban engedÊlyezd a fotÃŗ Ês videÃŗ jogosultsÃĄgokat.", - "permission_onboarding_request": "EngedÊlyezni kell, hogy az Immich hozzÃĄfÊrjen a kÊpeidhez Ês videÃŗidhoz", + "permission_onboarding_request": "EngedÊlyezni kell, hogy az Immich hozzÃĄfÊrjen a kÊpeidhez Ês videÃŗidhoz.", "person": "SzemÊly", "person_birthdate": "SzÃŧletett: {date}", "person_hidden": "{name}{hidden, select, true { (rejtett)} other {}}", @@ -1304,19 +1393,25 @@ "play_memories": "EmlÊkek lejÃĄtszÃĄsa", "play_motion_photo": "MozgÃŗkÊp lejÃĄtszÃĄsa", "play_or_pause_video": "VideÃŗ elindítÃĄsa vagy megÃĄllítÃĄsa", + "port": "Port", "preferences_settings_subtitle": "AlkalmazÃĄsbeÃĄllítÃĄsok kezelÊse", "preferences_settings_title": "BeÃĄllítÃĄsok", "preset": "Sablon", "preview": "ElőnÊzet", "previous": "Előző", "previous_memory": "Előző emlÊk", - "previous_or_next_photo": "Előző vagy kÃļvetkező fotÃŗ", + "previous_or_next_day": "Nap előre/hÃĄtra", + "previous_or_next_month": "HÃŗnap előre/hÃĄtra", + "previous_or_next_photo": "FotÃŗ előre/hÃĄtra", + "previous_or_next_year": "Év előre/hÃĄtra", "primary": "Elsődleges", "privacy": "MagÃĄnszfÊra", + "profile": "Profil", "profile_drawer_app_logs": "NaplÃŗk", "profile_drawer_client_out_of_date_major": "A mobilalkalmazÃĄs elavult. KÊrjÃŧk, frissítsd a legfrisebb főverziÃŗra.", "profile_drawer_client_out_of_date_minor": "A mobilalkalmazÃĄs elavult. KÊrjÃŧk, frissítsd a legfrisebb alverziÃŗra.", "profile_drawer_client_server_up_to_date": "A Kliens Ês a Szerver is naprakÊsz", + "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "A szerver elavult. KÊrjÃŧk, frissítsd a legfrisebb főverziÃŗra.", "profile_drawer_server_out_of_date_minor": "A szerver elavult. KÊrjÃŧk, frissítsd a legfrisebb alverziÃŗra.", "profile_image_of_user": "{user} profilkÊpe", @@ -1370,6 +1465,8 @@ "recent_searches": "LegutÃŗbbi keresÊsek", "recently_added": "NemrÊg hozzÃĄadott", "recently_added_page_title": "NemrÊg HozzÃĄadott", + "recently_taken": "NemrÊg kÊszített", + "recently_taken_page_title": "NemrÊg kÊszített", "refresh": "FrissítÊs", "refresh_encoded_videos": "ÁtkÃŗdolt videÃŗk frissítÊse", "refresh_faces": "Arcok frissítÊse", @@ -1389,9 +1486,12 @@ "remove_deleted_assets": "TÃļrÃļlt Elemek EltÃĄvolítÃĄsa", "remove_from_album": "EltÃĄvolítÃĄs az albumbÃŗl", "remove_from_favorites": "EltÃĄvolítÃĄs a kedvencekből", + "remove_from_locked_folder": "EltÃĄvolítÃĄs a zÃĄrolt mappÃĄbÃŗl", + "remove_from_locked_folder_confirmation": "Biztosan ki szeretnÊd venni ezeket a fotÃŗkat Ês videÃŗkat a zÃĄrolt mappÃĄbÃŗl? LÃĄthatÃŗak lesznek a kÃļnyvtÃĄradban.", "remove_from_shared_link": "EltÃĄvolítÃĄs a megosztott linkből", "remove_memory": "EmlÊk eltÃĄvolítÃĄsa", "remove_photo_from_memory": "KÊp eltÃĄvolítÃĄsa az emlÊkből", + "remove_tag": "Címke eltÃĄvolítÃĄsa", "remove_url": "URL eltÃĄvolítÃĄsa", "remove_user": "FelhasznÃĄlÃŗ eltÃĄvolítÃĄsa", "removed_api_key": "API Kulcs eltÃĄvolítva: {name}", @@ -1485,7 +1585,7 @@ "search_result_page_new_search_hint": "Új KeresÊs", "search_settings": "KeresÊsi beÃĄllítÃĄsok", "search_state": "Megye/Állam keresÊse...", - "search_suggestion_list_smart_search_hint_1": "Az intelligens keresÊs alapÊrtelmezetten be van kapcsolva, metaadatokat így kereshetsz: ", + "search_suggestion_list_smart_search_hint_1": "Az intelligens keresÊs alapÊrtelmezetten be van kapcsolva, metaadatokat így kereshetsz ", "search_suggestion_list_smart_search_hint_2": "m:keresÊsi-kifejezÊs", "search_tags": "CímkÊk keresÊse...", "search_timezone": "IdőzÃŗna keresÊse...", @@ -1517,6 +1617,7 @@ "server_info_box_server_url": "Szerver Címe", "server_offline": "Szerver Nem ElÊrhető", "server_online": "Szerver ElÊrhető", + "server_privacy": "Szerver biztonsÃĄg", "server_stats": "Szerver StatisztikÃĄk", "server_version": "Szerver VerziÃŗ", "set": "BeÃĄllít", @@ -1526,6 +1627,7 @@ "set_date_of_birth": "SzÃŧletÊsi dÃĄtum beÃĄllítÃĄsa", "set_profile_picture": "ProfilkÊp beÃĄllítÃĄsa", "set_slideshow_to_fullscreen": "DiavetítÊs teljes kÊpernyőre ÃĄllítÃĄsa", + "set_stack_primary_asset": "BeÃĄllítÃĄs elsődleges elemkÊnt", "setting_image_viewer_help": "Az Elem Megjelenítő előszÃļr a kis bÊlyegkÊpet tÃļlti be, aztÃĄn a kÃļzepes mÊretÅą előnÊzetet (ha elÊrhető), vÊgÃŧl az eredetit (ha elÊrhető).", "setting_image_viewer_original_subtitle": "EngedÊlyezi az eredeti teljes felbontÃĄsÃē kÊp betÃļltÊsÊt (nagy!). Kikapcsolva csÃļkkenti az adathasznÃĄlatot (a neten Ês az eszkÃļz gyorsítÃŗtÃĄrÃĄn is).", "setting_image_viewer_original_title": "Eredeti kÊp betÃļltÊse", @@ -1556,6 +1658,7 @@ "share_add_photos": "FotÃŗk hozzÃĄadÃĄsa", "share_assets_selected": "{count} kivÃĄlasztva", "share_dialog_preparing": "ElőkÊszítÊs...", + "share_link": "Link megosztÃĄsa", "shared": "Megosztva", "shared_album_activities_input_disable": "HozzÃĄszÃŗlÃĄsok kikapcsolva", "shared_album_activity_remove_content": "TÃļrÃļlni szeretnÊd ezt a tevÊkenysÊget?", @@ -1595,6 +1698,7 @@ "shared_link_expires_second": "{count} mÃĄsodperc mÃēlva lejÃĄr", "shared_link_expires_seconds": "{count} mÃĄsodperc mÃēlva lejÃĄr", "shared_link_individual_shared": "EgyÊnileg megosztva", + "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Megosztott linkek kezelÊse", "shared_link_options": "Megosztott link beÃĄllítÃĄsai", "shared_links": "Megosztott linkek", @@ -1656,6 +1760,7 @@ "stack_select_one_photo": "VÃĄlassz egy fő kÊpet a csoportbÃŗl", "stack_selected_photos": "KivÃĄlasztott fÊnykÊpek csoportosítÃĄsa", "stacked_assets_count": "{count, plural, other {# elem}} csoportosítva", + "stacktrace": "Hiba leírÃĄsa", "start": "Elindít", "start_date": "Kezdő dÃĄtum", "state": "Megye/Állam", @@ -1666,6 +1771,7 @@ "stop_sharing_photos_with_user": "FÊnykÊpeid megosztÃĄsÃĄnak megszÃŧntetÊse ezzel a felhasznÃĄlÃŗval", "storage": "TÃĄrhely", "storage_label": "TÃĄrhely címke", + "storage_quota": "TÃĄrhely kvÃŗta", "storage_usage": "{used}/{available} hasznÃĄlatban", "submit": "BekÃŧldÊs", "suggestions": "Javaslatok", @@ -1693,11 +1799,11 @@ "theme_selection_description": "A bÃļngÊsző beÃĄllítÃĄsÃĄnak megfelelően automatikusan hasznÃĄljon vilÃĄgos vagy sÃļtÊt tÊmÃĄt", "theme_setting_asset_list_storage_indicator_title": "TÃĄrhely ikon mutatÃĄsa az elemeken", "theme_setting_asset_list_tiles_per_row_title": "Elemek szÃĄma soronkÊnt ({count})", - "theme_setting_colorful_interface_subtitle": "AlapÊrtelmezett szín hasznÃĄlata a hÃĄttÊrben lÊvő felÃŧletekhez", + "theme_setting_colorful_interface_subtitle": "AlapÊrtelmezett szín hasznÃĄlata a hÃĄttÊrben lÊvő felÃŧletekhez.", "theme_setting_colorful_interface_title": "Színes felhasznÃĄlÃŗi felÃŧlet", "theme_setting_image_viewer_quality_subtitle": "RÊszletes kÊpmegjelenítő minősÊgÊnek beÃĄllítÃĄsa", "theme_setting_image_viewer_quality_title": "KÊpmegjelenítő minősÊge", - "theme_setting_primary_color_subtitle": "VÃĄlassz egy színt az alapÊrtelmezett mÅąveletekhez Ês kiemelÊsekhez", + "theme_setting_primary_color_subtitle": "VÃĄlassz egy színt az alapÊrtelmezett mÅąveletekhez Ês kiemelÊsekhez.", "theme_setting_primary_color_title": "AlapÊrtelmezett szín", "theme_setting_system_primary_color_title": "Rendszerszínek hasznÃĄlata", "theme_setting_system_theme_switch": "Automatikus (kÃļveti a rendszer tÊmÃĄjÃĄt)", @@ -1737,6 +1843,7 @@ "unable_to_setup_pin_code": "Sikertelen PIN kÃŗd beÃĄllítÃĄs", "unarchive": "ArchívumbÃŗl kivesz", "unarchived_count": "{count, plural, other {# elem kivÊve az archívumbÃŗl}}", + "undo": "VisszavonÃĄs", "unfavorite": "Kedvenc kÃļzÃŧl kivesz", "unhide_person": "Nem rejtett szemÊly", "unknown": "Ismeretlen", @@ -1756,6 +1863,7 @@ "unstack": "Csoport SzÊtszedÊse", "unstacked_assets_count": "{count, plural, other {# elemből}} ÃĄllÃŗ csoport szÊtszedve", "up_next": "KÃļvetkezik", + "updated_at": "Frissített", "updated_password": "JelszÃŗ megvÃĄltoztatva", "upload": "FeltÃļltÊs", "upload_concurrency": "PÃĄrhuzamos feltÃļltÊs", @@ -1770,14 +1878,17 @@ "upload_success": "FeltÃļltÊs sikeres, frissítsd az oldalt az Ãējonnan feltÃļltÃļtt elemek megtekintÊsÊhez.", "upload_to_immich": "FeltÃļltÊs Immich-be ({count})", "uploading": "FeltÃļltÊs folyamatban", + "url": "URL", "usage": "HasznÃĄlat", "use_current_connection": "Jelenlegi kapcsolat hasznÃĄlata", "use_custom_date_range": "Szabadon megadott időintervallum hasznÃĄlata", "user": "FelhasznÃĄlÃŗ", + "user_has_been_deleted": "Ez a felhasznÃĄlÃŗ tÃļrlÊsre kerÃŧlt.", "user_id": "FelhasznÃĄlÃŗ azonosítÃŗja", "user_liked": "{user} felhasznÃĄlÃŗnak {type, select, photo {ez a fÊnykÊp} video {ez a videÃŗ} asset {ez az elem} other {ez}} tetszik", "user_pin_code_settings": "PIN kÃŗd", "user_pin_code_settings_description": "PIN kÃŗd kezelÊse", + "user_privacy": "FelhasznÃĄlÃŗi adatvÊdelem", "user_purchase_settings": "MegvÃĄsÃĄrlÃĄs", "user_purchase_settings_description": "VÃĄsÃĄrlÃĄs kezelÊse", "user_role_set": "{user} felhasznÃĄlÃŗnak {role} jogkÃļr biztosítÃĄsa", @@ -1822,12 +1933,12 @@ "week": "HÊt", "welcome": "ÜdvÃļzlÃŧnk", "welcome_to_immich": "ÜdvÃļzÃļl az Immich", - "wifi_name": "WiFi Neve", + "wifi_name": "Wi-Fi Neve", "wrong_pin_code": "HibÃĄs PIN kÃŗd", "year": "Év", "years_ago": "{years, plural, one {# Êvvel} other {# Êvvel}} ezelőtt", "yes": "Igen", "you_dont_have_any_shared_links": "Nincsenek megosztott linkjeid", - "your_wifi_name": "A WiFi hÃĄlÃŗzatod neve", + "your_wifi_name": "A Wi-Fi hÃĄlÃŗzatod neve", "zoom_image": "KÊp NagyítÃĄsa" } diff --git a/i18n/id.json b/i18n/id.json index 4d15ef01e9..92cb07ff30 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -34,6 +34,7 @@ "added_to_favorites_count": "Ditambahkan {count, number} ke favorit", "admin": { "add_exclusion_pattern_description": "Tambahkan pola pengecualian. Glob menggunakan *, **, dan ? didukung. Untuk mengabaikan semua berkas dalam direktori apa pun bernama \"Raw\", gunakan \"**/Raw/**\". Untuk mengabaikan semua berkas berakhiran dengan \".tif\", gunakan \"**/*.tif\". Untuk mengabaikan jalur absolut, gunakan \"/jalur/untuk/diabaikan/**\".", + "admin_user": "Pengguna Admin", "asset_offline_description": "Aset pustaka eksternal ini tidak ada di diska dan telah dipindahkan ke tempat sampah. Jika berkasnya dipindah dalam pustaka, periksa lini masa Anda untuk aset baru yang cocok. Untuk memulihkan aset ini, pastikan jalur berkas di bawah dapat diakses oleh Immich dan pindai pustaka.", "authentication_settings": "Pengaturan Autentikasi", "authentication_settings_description": "Kelola kata sandi, OAuth, dan pengaturan autentikasi lainnya", @@ -44,7 +45,7 @@ "backup_database_enable_description": "Aktifkan pencadangan basis data", "backup_keep_last_amount": "Jumlah cadangan untuk disimpan", "backup_settings": "Pengaturan Pencadangan Basis Data", - "backup_settings_description": "Kelola pengaturan pencadangan basis data. Catatan: Tugas ini tidak dipantau dan Anda tidak akan diberi tahu jika ada kesalahan.", + "backup_settings_description": "Kelola pengaturan pencadangan basis data.", "cleared_jobs": "Tugas terselesaikan untuk: {job}", "config_set_by_file": "Konfigurasi saat ini ditetapkan oleh berkas konfigurasi", "confirm_delete_library": "Apakah Anda yakin ingin menghapus pustaka {library}?", @@ -165,12 +166,26 @@ "metadata_settings_description": "Kelola pengaturan metadata", "migration_job": "Migrasi", "migration_job_description": "Migrasikan gambar kecil untuk aset dan wajah ke struktur folder terkini", + "nightly_tasks_cluster_faces_setting_description": "Mulai pengenalan wajah pada semua wajah yang baru saja terdeteksi", + "nightly_tasks_cluster_new_faces_setting": "Kelompokkan semua wajah baru", + "nightly_tasks_database_cleanup_setting": "Tugas pembersihan basis data", + "nightly_tasks_database_cleanup_setting_description": "Membersihkan data lama, kadaluarsa dari database", + "nightly_tasks_generate_memories_setting": "Buat kenang-kenangan", + "nightly_tasks_generate_memories_setting_description": "Buat kenang-kenangan baru dari berbagai aset", + "nightly_tasks_missing_thumbnails_setting": "Membuat thumbnail yang hilang", + "nightly_tasks_missing_thumbnails_setting_description": "Mengantrikan aset tanpa thumbnail untuk pembuatan thumbnail", + "nightly_tasks_settings": "Pengaturan Tugas Malam", + "nightly_tasks_settings_description": "Atur tugas malam", + "nightly_tasks_start_time_setting": "Waktu mulai", + "nightly_tasks_start_time_setting_description": "Waktu saat server mulai menjalankan tugas malam", + "nightly_tasks_sync_quota_usage_setting": "Sinkronisasi penggunaan kuota", + "nightly_tasks_sync_quota_usage_setting_description": "Pembaruan kuota penyimpanan pengguna, berdasarkan penggunaan sekarang", "no_paths_added": "Tidak ada jalur yang ditambahkan", "no_pattern_added": "Tidak ada pola yang ditambahkan", "note_apply_storage_label_previous_assets": "Catatan: Untuk menerapkan Label Penyimpanan untuk aset yang telah diunggah sebelumnya, jalankan", "note_cannot_be_changed_later": "CATATAN: Ini tidak akan dapat diubah lagi!", "notification_email_from_address": "Dari alamat", - "notification_email_from_address_description": "Alamat surel pengirim, misalnya: \"Server Foto Immich \"", + "notification_email_from_address_description": "Alamat surel pengirim, misalnya: \"Server Foto Immich \". Pastikan untuk menggunakan alamat yang diizinkan untuk mengirim email.", "notification_email_host_description": "Hos server surel (mis. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Abaikan eror sertifikat", "notification_email_ignore_certificate_errors_description": "Abaikan eror validasi sertifikat TLS (tidak disarankan)", @@ -195,6 +210,7 @@ "oauth_mobile_redirect_uri": "URI pengalihan ponsel", "oauth_mobile_redirect_uri_override": "Penimpaan URI penerusan ponsel", "oauth_mobile_redirect_uri_override_description": "Aktifkan ketika provider OAuth tidak mengizinkan tautan mobile, seperti ''{callback}''", + "oauth_role_claim_description": "Secara otomatis memberikan akses admin berdasarkan keberadaan klaim ini. Klaim dapat berupa \"user\" atau \"admin\".", "oauth_settings": "OAuth", "oauth_settings_description": "Kelola pengaturan log masuk OAuth", "oauth_settings_more_details": "Untuk detail lanjut tentang fitur ini, lihat docs.", @@ -243,6 +259,7 @@ "storage_template_migration_info": "Templat penyimpanan akan mengubah semua ekstensi ke huruf kecil. Perubahan templat hanya akan diterapkan pada aset baru. Untuk menerapkan templat pada setiap aset yang sebelumnya telah diunggah, jalankan {job}.", "storage_template_migration_job": "Tugas Migrasi Templat Ruang Penyimpanan", "storage_template_more_details": "Untuk detail lebih lanjut tentang fitur ini, pergi ke Templat Penyimpanan dan kekurangannya", + "storage_template_onboarding_description_v2": "Saat diaktifkan, fitur ini akan mengatur file secara otomatis berdasarkan templat yang ditentukan pengguna. Untuk informasi selengkapnya, silakan lihat dokumentasi.", "storage_template_path_length": "Batas panjang jalur: {length, number}{limit, number}", "storage_template_settings": "Templat Penyimpanan", "storage_template_settings_description": "Kelola struktur folder dan nama berkas dari aset yang diunggah", @@ -257,7 +274,7 @@ "template_email_update_album": "Perbarui Templat Album", "template_email_welcome": "Templat surel selamat datang", "template_settings": "Templat Notifikasi", - "template_settings_description": "Kelola templat kustom untuk notifikasi.", + "template_settings_description": "Kelola templat kustom untuk notifikasi", "theme_custom_css_settings": "CSS Kustom", "theme_custom_css_settings_description": "CSS memungkinkan desain Immich untuk diubah.", "theme_settings": "Pengaturan Tema", @@ -355,13 +372,20 @@ "admin_password": "Kata Sandi Admin", "administration": "Administrasi", "advanced": "Tingkat lanjut", + "advanced_settings_beta_timeline_subtitle": "Coba pengalaman aplikasi baru", + "advanced_settings_beta_timeline_title": "Garis waktu Beta", "advanced_settings_enable_alternate_media_filter_subtitle": "Gunakan opsi ini untuk menyaring media saat sinkronisasi berdasarkan kriteria alternatif. Hanya coba ini dengan aplikasi mendeteksi semua album.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTAL] Gunakan saringan sinkronisasi album perangkat alternatif", "advanced_settings_log_level_title": "Tingkat log: {level}", "advanced_settings_prefer_remote_subtitle": "Beberapa perangkat tidak dapat memuat gambar kecil dengan cepat. Menyalakan ini akan memuat gambar kecil dari server.", "advanced_settings_prefer_remote_title": "Prioritaskan gambar dari server", + "advanced_settings_proxy_headers_subtitle": "Tentukan header proxy yang harus dikirim Immich dengan setiap permintaan jaringan", + "advanced_settings_self_signed_ssl_subtitle": "Melewati verifikasi sertifikat SSL untuk titik akhir server. Diperlukan untuk sertifikat yang ditandatangani sendiri.", + "advanced_settings_self_signed_ssl_title": "Izinkan sertifikat SSL yang ditandatangani sendiri", "advanced_settings_sync_remote_deletions_subtitle": "Hapus atau pulihkan aset pada perangkat ini secara otomatis ketika tindakan dilakukan di web", "advanced_settings_sync_remote_deletions_title": "Sinkronisasi penghapusan jarak jauh [EKSPERIMENTAL]", + "advanced_settings_tile_subtitle": "Pengaturan pengguna tingkat lanjut", + "advanced_settings_troubleshooting_subtitle": "Aktifkan fitur tambahan untuk pemecahan masalah", "age_months": "Umur {months, plural, one {# bulan} other {# bulan}}", "age_year_months": "Umur 1 tahun, {months, plural, one {# bulan} other {# bulan}}", "age_years": "{years, plural, other {Umur #}}", @@ -446,7 +470,6 @@ "assets": "Aset", "assets_added_count": "{count, plural, one {# aset} other {# aset}} ditambahkan", "assets_added_to_album_count": "Ditambahkan {count, plural, one {# aset} other {# aset}} ke album", - "assets_added_to_name_count": "Ditambahkan {count, plural, one {# aset} other {# aset}} ke {hasName, select, true {{name}} other {album baru}}", "assets_count": "{count, plural, one {# aset} other {# aset}}", "assets_deleted_permanently": "{count} aset dihapus secara permanen", "assets_deleted_permanently_from_server": "{count} aset dihapus secara permanen dari server Immich", diff --git a/i18n/it.json b/i18n/it.json index 07e19736a7..0aec538a85 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -87,7 +87,7 @@ "image_settings": "Impostazioni delle immagini", "image_settings_description": "Gestisci qualità e risoluzione delle immagini generate", "image_thumbnail_description": "Miniatura piccola senza metadati, utilizzata durante la visualizzazione di gruppi di foto come la sequenza temporale principale", - "image_thumbnail_quality_description": "Qualità delle miniature da 1 a 100. Un valore piÚ alto è migliore, ma produce file piÚ grandi e puÃ˛ ridurre la reattività dell'app.", + "image_thumbnail_quality_description": "Qualità delle anteprime da 1 a 100. Un valore piÚ alto è migliore ma produce file piÚ grandi e puÃ˛ ridurre la reattività dell'app.", "image_thumbnail_title": "Impostazioni della copertina", "job_concurrency": "Concorrenza {job}", "job_created": "Processo creato", @@ -105,7 +105,7 @@ "library_scanning_enable_description": "Attiva la scansione periodica della libreria", "library_settings": "Libreria Esterna", "library_settings_description": "Gestisci le impostazioni della libreria esterna", - "library_tasks_description": "Scansiona le librerie esterne per i nuovi aggiornamenti", + "library_tasks_description": "Scansiona le librerie esterne per risorse nuove o modificate", "library_watching_enable_description": "Osserva le librerie esterne per cambiamenti", "library_watching_settings": "Osserva librerie (SPERIMENTALE)", "library_watching_settings_description": "Osserva automaticamente i cambiamenti dei file", @@ -121,7 +121,7 @@ "machine_learning_enabled": "Attiva machine learning", "machine_learning_enabled_description": "Se disabilitato, tutte le funzioni di ML saranno disabilitate ignorando le importazioni sottostanti.", "machine_learning_facial_recognition": "Riconoscimento Facciale", - "machine_learning_facial_recognition_description": "Rileva, riconosci, e raggruppa facce nelle immagini", + "machine_learning_facial_recognition_description": "Rileva, riconosci e raggruppa volti nelle immagini", "machine_learning_facial_recognition_model": "Modello di riconoscimento facciale", "machine_learning_facial_recognition_model_description": "I modelli sono mostrati in ordine decrescente in base alla dimensione. I modelli piÚ grandi sono piÚ lenti e utilizzano piÚ memoria, perÃ˛ producono risultati migliori. Nota che devi ri-eseguire il processo di rilevamento facciale per tutte le immagini quando cambi il modello.", "machine_learning_facial_recognition_setting": "Attiva riconoscimento facciale", @@ -156,16 +156,30 @@ "map_settings": "Impostazioni Mappa e Posizione", "map_settings_description": "Gestisci impostazioni mappa", "map_style_description": "URL per un tema della mappa style.json", - "memory_cleanup_job": "pulizia memoria", - "memory_generate_job": "Generazione della memoria", + "memory_cleanup_job": "Pulizia dei vecchi Ricordi", + "memory_generate_job": "Generazione dei Ricordi", "metadata_extraction_job": "Estrazione Metadata", - "metadata_extraction_job_description": "Estrai informazioni dai metadati di ciascun asset, ad esempio coordinate GPS, volti e risoluzione", + "metadata_extraction_job_description": "Estrai informazioni dai metadati di ciascuna risorsa, come coordinate GPS, volti e risoluzione", "metadata_faces_import_setting": "Abilita l'importazione dei volti", "metadata_faces_import_setting_description": "Importa i volti dai dati EXIF dell'immagine e dai file sidecar", "metadata_settings": "Impostazioni Metadati", "metadata_settings_description": "Gestisci le impostazioni dei metadati", "migration_job": "Migrazione", "migration_job_description": "Migra le anteprime per gli asset e volti alla struttura di cartelle piÚ recente", + "nightly_tasks_cluster_faces_setting_description": "Avvia riconoscimento facciale sui volti appena rilevati", + "nightly_tasks_cluster_new_faces_setting": "Raggruppa nuovi volti", + "nightly_tasks_database_cleanup_setting": "Processi di pulizia del database", + "nightly_tasks_database_cleanup_setting_description": "Ripulisci il database da file vecchi e scaduti", + "nightly_tasks_generate_memories_setting": "Genera ricordi", + "nightly_tasks_generate_memories_setting_description": "Genera nuovi ricordi a partire dalle risorse", + "nightly_tasks_missing_thumbnails_setting": "Genera anteprime mancanti", + "nightly_tasks_missing_thumbnails_setting_description": "Metti in coda le risorse senza miniatura per la generazione delle anteprime", + "nightly_tasks_settings": "Impostazioni delle attività notturne", + "nightly_tasks_settings_description": "Gestisci attività notturne", + "nightly_tasks_start_time_setting": "Tempo di avvio", + "nightly_tasks_start_time_setting_description": "Il tempo in cui il server fa partire le attività notturne", + "nightly_tasks_sync_quota_usage_setting": "Sincronizza la quota di utilizzo", + "nightly_tasks_sync_quota_usage_setting_description": "Aggiorna la quota di spazio dell'utente in base all'utilizzo corrente", "no_paths_added": "Nessun percorso aggiunto", "no_pattern_added": "Nessun pattern aggiunto", "note_apply_storage_label_previous_assets": "Nota: Per assegnare l'etichetta storage ad asset precedentemente caricati, esegui", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "URI reindirizzamento mobile", "oauth_mobile_redirect_uri_override": "Sovrascrivi URI reindirizzamento cellulare", "oauth_mobile_redirect_uri_override_description": "Abilita quando il gestore OAuth non consente un URL come ''{callback}''", + "oauth_role_claim": "Claim del ruolo", + "oauth_role_claim_description": "Concedi automaticamente l'accesso come amministratore in base alla presenza di questo claim. Il claim puÃ˛ essere 'user' o 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "Gestisci impostazioni di login OAuth", "oauth_settings_more_details": "Per piÚ dettagli riguardo a questa funzionalità, consulta la documentazione.", @@ -264,8 +280,8 @@ "theme_custom_css_settings_description": "I Cascading Style Sheets (CSS) permettono di personalizzare l'interfaccia di Immich.", "theme_settings": "Impostazioni Tema", "theme_settings_description": "Gestisci la personalizzazione dell'interfaccia web di Immich", - "thumbnail_generation_job": "Generazione Miniature", - "thumbnail_generation_job_description": "Genera miniature grandi, piccole e sfocate per ogni asset, oltre a miniature per ogni persona", + "thumbnail_generation_job": "Genera Anteprime", + "thumbnail_generation_job_description": "Genera anteprime grandi, piccole e sfocate per ogni asset, oltre a miniature per ogni persona", "transcoding_acceleration_api": "API di accelerazione", "transcoding_acceleration_api_description": "L'API che interagirà con il tuo dispositivo per accelerare la transcodifica. Questa impostazione è \"best effort\": ripiegherà sulla transcodifica software in caso di fallimento. VP9 potrebbe funzionare o meno a seconda del tuo hardware.", "transcoding_acceleration_nvenc": "NVENC (richiede GPU NVIDIA)", @@ -357,25 +373,27 @@ "admin_password": "Password Amministratore", "administration": "Amministrazione", "advanced": "Avanzate", + "advanced_settings_beta_timeline_subtitle": "Prova la nuova esperienza dell'app", + "advanced_settings_beta_timeline_title": "Timeline beta", "advanced_settings_enable_alternate_media_filter_subtitle": "Usa questa opzione per filtrare i contenuti multimediali durante la sincronizzazione in base a criteri alternativi. Prova questa opzione solo se riscontri problemi con il rilevamento di tutti gli album da parte dell'app.", "advanced_settings_enable_alternate_media_filter_title": "[SPERIMENTALE] Usa un filtro alternativo per la sincronizzazione degli album del dispositivo", "advanced_settings_log_level_title": "Livello log: {level}", - "advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini dal dispositivo. Attivare questa impostazione per caricare invece le immagini remote.", + "advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini locali. Attivare questa impostazione per caricare invece le immagini remote.", "advanced_settings_prefer_remote_title": "Preferisci immagini remote", "advanced_settings_proxy_headers_subtitle": "Definisci gli header per i proxy che Immich dovrebbe inviare con ogni richiesta di rete", "advanced_settings_proxy_headers_title": "Header Proxy", "advanced_settings_self_signed_ssl_subtitle": "Salta la verifica dei certificati SSL del server. Richiesto con l'uso di certificati self-signed.", "advanced_settings_self_signed_ssl_title": "Consenti certificati SSL self-signed", - "advanced_settings_sync_remote_deletions_subtitle": "Rimuovi o ripristina automaticamente un elemento su questo dispositivo se l'azione è stata fatta via web", + "advanced_settings_sync_remote_deletions_subtitle": "Rimuovi o ripristina automaticamente un elemento su questo dispositivo quando l'azione è stata fatta via web", "advanced_settings_sync_remote_deletions_title": "Sincronizza le cancellazioni remote [SPERIMENTALE]", - "advanced_settings_tile_subtitle": "Impostazioni aggiuntive utenti", + "advanced_settings_tile_subtitle": "Impostazioni avanzate dell'utente", "advanced_settings_troubleshooting_subtitle": "Attiva funzioni addizionali per la risoluzione dei problemi", "advanced_settings_troubleshooting_title": "Risoluzione problemi", "age_months": "Età {months, plural, one {# mese} other {# mesi}}", "age_year_months": "Età 1 anno, {months, plural, one {# mese} other {# mesi}}", "age_years": "{years, plural, one {# anno} other {# anni}}", "album_added": "Album aggiunto", - "album_added_notification_setting_description": "Ricevi una notifica email quando sei aggiunto a un album condiviso", + "album_added_notification_setting_description": "Ricevi una notifica email quando sei aggiunto ad un album condiviso", "album_cover_updated": "Copertina dell'album aggiornata", "album_delete_confirmation": "Sei sicuro di voler cancellare l'album {album}?", "album_delete_confirmation_description": "Se l'album è condiviso gli altri utenti perderanno l'accesso.", @@ -388,38 +406,40 @@ "album_options": "Impostazioni Album", "album_remove_user": "Rimuovi l'utente?", "album_remove_user_confirmation": "Sicuro di voler rimuovere l'utente {user}?", + "album_search_not_found": "Nessun album trovato corrispondente alla tua ricerca", "album_share_no_users": "Sembra che tu abbia condiviso questo album con tutti gli utenti oppure non hai nessun utente con cui condividere.", "album_updated": "Album aggiornato", "album_updated_setting_description": "Ricevi una notifica email quando un album condiviso ha nuovi media", "album_user_left": "{album} abbandonato", "album_user_removed": "Utente {user} rimosso", "album_viewer_appbar_delete_confirm": "Sei sicuro di voler rimuovere questo album dal tuo account?", - "album_viewer_appbar_share_err_delete": "Impossibile eliminare l'album", - "album_viewer_appbar_share_err_leave": "Impossibile lasciare l'album", - "album_viewer_appbar_share_err_remove": "Ci sono problemi nel rimuovere oggetti dall'album", - "album_viewer_appbar_share_err_title": "Impossibile cambiare il titolo dell'album", + "album_viewer_appbar_share_err_delete": "Non è stato possibile eliminare l'album", + "album_viewer_appbar_share_err_leave": "Non è stato possibile lasciare l'album", + "album_viewer_appbar_share_err_remove": "Ci sono problemi nel rimuovere elementi dall'album", + "album_viewer_appbar_share_err_title": "Non è stato possibile cambiare il titolo dell'album", "album_viewer_appbar_share_leave": "Lascia album", "album_viewer_appbar_share_to": "Condividi a", "album_viewer_page_share_add_users": "Aggiungi utenti", "album_with_link_access": "Permetti a chiunque possieda il link di visualizzare le foto e le persone dell'album.", "albums": "Album", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Album}}", - "albums_default_sort_order": "Ordinamento album predefinito", - "albums_default_sort_order_description": "Ordine iniziale degli asset alla creazione di nuovi album.", - "albums_feature_description": "Collezione di asset che possono essere condivisi con altri utenti.", + "albums_default_sort_order": "Ordinamento predefinito degli album", + "albums_default_sort_order_description": "Ordine iniziale degli elementi alla creazione di nuovi album.", + "albums_feature_description": "Raggruppamento di elementi che possono essere condivisi con altri utenti.", + "albums_on_device_count": "Album sul dispositivo ({count})", "all": "Tutti", "all_albums": "Tutti gli album", "all_people": "Tutte le persone", "all_videos": "Tutti i video", "allow_dark_mode": "Permetti Tema Scuro", - "allow_edits": "Permetti Modifiche", + "allow_edits": "Permetti modifiche", "allow_public_user_to_download": "Permetti agli utenti pubblici di scaricare", "allow_public_user_to_upload": "Permetti agli utenti pubblici di caricare", "alt_text_qr_code": "Immagine QR", "anti_clockwise": "Senso anti-orario", "api_key": "Chiave API", - "api_key_description": "Il valore verrà mostrato solo una volta. Assicurati di copiarlo prima di chiudere la finestra.", - "api_key_empty": "Il nome della chiave API non puÃ˛ essere vuoto", + "api_key_description": "Questo valore verrà mostrato una sola volta. Assicurati di copiarlo prima di chiudere la finestra.", + "api_key_empty": "Il nome della chiave API non dovrebbe essere vuoto", "api_keys": "Chiavi API", "app_bar_signout_dialog_content": "Sei sicuro di volerti disconnettere?", "app_bar_signout_dialog_ok": "Si", @@ -427,8 +447,9 @@ "app_settings": "Impostazioni Applicazione", "appears_in": "Compare in", "archive": "Archivio", + "archive_action_prompt": "Aggiunti {count} elementi all'Archivio", "archive_or_unarchive_photo": "Archivia o ripristina foto", - "archive_page_no_archived_assets": "Nessuna oggetto archiviato", + "archive_page_no_archived_assets": "Non è stato trovato nessun elemento archiviato", "archive_page_title": "Archivio ({count})", "archive_size": "Dimensioni Archivio", "archive_size_description": "Imposta le dimensioni dell'archivio per i download (in GiB)", @@ -440,42 +461,41 @@ "asset_action_share_err_offline": "Non è possibile recuperare le risorse offline, azione ignorata", "asset_added_to_album": "Aggiunto all'album", "asset_adding_to_album": "Aggiungendo all'albumâ€Ļ", - "asset_description_updated": "La descrizione del media è stata aggiornata", + "asset_description_updated": "La descrizione dell'elemento è stata aggiornata", "asset_filename_is_offline": "Il media {filename} è offline", "asset_has_unassigned_faces": "Il media ha dei volti non categorizzati", "asset_hashing": "Hashing in corso â€Ļ", "asset_list_group_by_sub_title": "Raggruppa per", "asset_list_layout_settings_dynamic_layout_title": "Layout dinamico", "asset_list_layout_settings_group_automatically": "Automatico", - "asset_list_layout_settings_group_by": "Raggruppa le risorse per", + "asset_list_layout_settings_group_by": "Raggruppa gli elementi per", "asset_list_layout_settings_group_by_month_day": "Mese + giorno", "asset_list_layout_sub_title": "Layout", - "asset_list_settings_subtitle": "Impostazion del layout della griglia delle foto", + "asset_list_settings_subtitle": "Impostazioni del layout della griglia delle foto", "asset_list_settings_title": "Griglia foto", - "asset_offline": "Risorsa Offline", - "asset_offline_description": "Questo media non è stato trovato nel disco. Contatta il tuo amministratore di Immich per assistenza.", - "asset_restored_successfully": "Asset ripristinato con successo", + "asset_offline": "Elemento Offline", + "asset_offline_description": "Questo elemento esterno non viene piÚ trovato sul disco. Contatta il tuo amministratore di Immich per assistenza.", + "asset_restored_successfully": "Elemento ripristinato con successo", "asset_skipped": "Saltato", "asset_skipped_in_trash": "Nel cestino", "asset_uploaded": "Caricato", "asset_uploading": "Caricamentoâ€Ļ", - "asset_viewer_settings_subtitle": "Gestisci le impostazioni del visualizzatore risorse", + "asset_viewer_settings_subtitle": "Gestisci le impostazioni del visualizzatore della galleria", "asset_viewer_settings_title": "Visualizzazione risorse", "assets": "Risorse", "assets_added_count": "{count, plural, one {# asset aggiunto} other {# asset aggiunti}}", "assets_added_to_album_count": "{count, plural, one {# asset aggiunto} other {# asset aggiunti}} all'album", - "assets_added_to_name_count": "Aggiunti {count, plural, one {# asset} other {# assets}} a {hasName, select, true {{name}} other {new album}}", - "assets_cannot_be_added_to_album_count": "{count, plural, one {L'asset} other {Gli asset}} non possono essere aggiunti all'album", - "assets_count": "{count, plural, other {# asset}}", + "assets_cannot_be_added_to_album_count": "{count, plural, one {L'elemento} other {Gli elementi}} non possono essere aggiunti all'album", + "assets_count": "{count, plural, one {# elemento} other {# elementi}}", "assets_deleted_permanently": "{count} elementi cancellati definitivamente", "assets_deleted_permanently_from_server": "{count} elementi cancellati definitivamente dal server Immich", - "assets_downloaded_failed": "{count, plural, one {Scaricato # file - {error} file non riusciti} other {Scaricati # file - {error} file non riusciti}}", + "assets_downloaded_failed": "{count, plural, one {Scaricato # file - {error} file non riuscito} other {Scaricati # file - {error} file non riusciti}}", "assets_downloaded_successfully": "{count, plural, one {Scaricato # file con successo} other {Scaricati # file con successo}}", - "assets_moved_to_trash_count": "{count, plural, one {# asset spostato} other {# asset spostati}} nel cestino", + "assets_moved_to_trash_count": "{count, plural, one {# elemento spostato} other {# elementi spostati}} nel cestino", "assets_permanently_deleted_count": "{count, plural, one {# asset cancellato} other {# asset cancellati}} definitivamente", "assets_removed_count": "{count, plural, one {# asset rimosso} other {# asset rimossi}}", "assets_removed_permanently_from_device": "{count} elementi cancellati definitivamente dal tuo dispositivo", - "assets_restore_confirmation": "Sei sicuro di voler ripristinare tutti gli asset cancellati? Non puoi annullare questa azione! Tieni presente che eventuali risorse offline NON possono essere ripristinate in questo modo.", + "assets_restore_confirmation": "Sei sicuro di voler ripristinare tutti gli elementi cancellati? Non puoi annullare questa azione! Tieni presente che eventuali risorse offline NON possono essere ripristinate in questo modo.", "assets_restored_count": "{count, plural, one {# asset ripristinato} other {# asset ripristinati}}", "assets_restored_successfully": "{count} elementi ripristinati", "assets_trashed": "{count} elementi cestinati", @@ -497,7 +517,7 @@ "backup_album_selection_page_selection_info": "Informazioni sulla selezione", "backup_album_selection_page_total_assets": "Numero totale delle risorse", "backup_all": "Tutti", - "backup_background_service_backup_failed_message": "Impossibile caricare i contenuti. Riprovoâ€Ļ", + "backup_background_service_backup_failed_message": "È stato impossibile fare il backup dei contenuti. Riprovoâ€Ļ", "backup_background_service_connection_failed_message": "Impossibile connettersi al server. Riprovoâ€Ļ", "backup_background_service_current_upload_notification": "Caricamento di {filename} in corso", "backup_background_service_default_notification": "Ricerca di nuovi contenutiâ€Ļ", @@ -506,7 +526,7 @@ "backup_background_service_upload_failure_notification": "Impossibile caricare {filename}", "backup_controller_page_albums": "Backup Album", "backup_controller_page_background_app_refresh_disabled_content": "Attiva l'aggiornamento dell'app in background in Impostazioni > Generale > Aggiorna app in background per utilizzare backup in background.", - "backup_controller_page_background_app_refresh_disabled_title": "Backup in background è disattivo", + "backup_controller_page_background_app_refresh_disabled_title": "Aggiornamento dell'app in background disattivo", "backup_controller_page_background_app_refresh_enable_button_text": "Vai alle impostazioni", "backup_controller_page_background_battery_info_link": "Mostrami come", "backup_controller_page_background_battery_info_message": "Per una migliore esperienza di backup, disabilita le ottimizzazioni della batteria per l'app Immich.\n\nDal momento che è una funzionalità specifica del dispositivo, per favore consulta il manuale del produttore.", @@ -515,12 +535,12 @@ "backup_controller_page_background_charging": "Solo durante la ricarica", "backup_controller_page_background_configure_error": "Impossibile configurare i servizi in background", "backup_controller_page_background_delay": "Ritarda il backup di nuovi elementi: {duration}", - "backup_controller_page_background_description": "Abilita i servizi in background per fare il backup di tutti i nuovi contenuti senza la necessità di aprire l'app", - "backup_controller_page_background_is_off": "Backup automatico disattivato", - "backup_controller_page_background_is_on": "Backup automatico attivo", + "backup_controller_page_background_description": "Abilita i servizi in background per fare il backup di nuovi contenuti senza la necessità di aprire l'app", + "backup_controller_page_background_is_off": "Backup automatico in background disattivato", + "backup_controller_page_background_is_on": "Backup automatico in background attivo", "backup_controller_page_background_turn_off": "Disabilita servizi in background", "backup_controller_page_background_turn_on": "Abilita servizi in background", - "backup_controller_page_background_wifi": "Solo Wi-Fi", + "backup_controller_page_background_wifi": "Solo con Wi-Fi", "backup_controller_page_backup": "Backup", "backup_controller_page_backup_selected": "Selezionati: ", "backup_controller_page_backup_sub": "Foto e video caricati", @@ -587,6 +607,7 @@ "cancel": "Annulla", "cancel_search": "Annulla ricerca", "canceled": "Annullato", + "canceling": "Annullamento", "cannot_merge_people": "Impossibile unire le persone", "cannot_undo_this_action": "Non puoi annullare questa azione!", "cannot_update_the_description": "Impossibile aggiornare la descrizione", @@ -703,7 +724,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Scuro", - "darkTheme": "Attiva/Disattiva tema scuro", + "dark_theme": "Imposta tema scuro", "date_after": "Data dopo", "date_and_time": "Data e ora", "date_before": "Data prima", @@ -719,6 +740,7 @@ "default_locale": "Localizzazione preimpostata", "default_locale_description": "Formatta la data e i numeri in base alle impostazioni del tuo browser", "delete": "Elimina", + "delete_action_prompt": "{count} elementi eliminati definitivamente", "delete_album": "Elimina album", "delete_api_key_prompt": "Sei sicuro di voler eliminare questa chiave API?", "delete_dialog_alert": "Questi oggetti saranno eliminati definitivamente da Immich e dal tuo device", @@ -732,6 +754,7 @@ "delete_key": "Elimina chiave", "delete_library": "Elimina libreria", "delete_link": "Elimina link", + "delete_local_action_prompt": "{count} elementi rimossi in locale", "delete_local_dialog_ok_backed_up_only": "Elimina solo con backup", "delete_local_dialog_ok_force": "Elimina comunque", "delete_others": "Elimina gli altri", @@ -745,6 +768,7 @@ "description": "Descrizione", "description_input_hint_text": "Aggiungi descrizione...", "description_input_submit_error": "Errore modificare descrizione, controlli I log per maggiori dettagli", + "deselect_all": "Deseleziona Tutto", "details": "Dettagli", "direction": "Direzione", "disabled": "Disabilitato", @@ -762,6 +786,7 @@ "documentation": "Documentazione", "done": "Fatto", "download": "Scarica", + "download_action_prompt": "Scaricando {count} elementi", "download_canceled": "Download annullato", "download_complete": "Download completato", "download_enqueue": "Download in coda", @@ -799,6 +824,7 @@ "edit_key": "Modifica chiave", "edit_link": "Modifica link", "edit_location": "Modifica posizione", + "edit_location_action_prompt": "{count} luoghi modificati", "edit_location_dialog_title": "Posizione", "edit_name": "Modifica nome", "edit_people": "Modifica persone", @@ -817,6 +843,7 @@ "empty_trash": "Svuota cestino", "empty_trash_confirmation": "Sei sicuro di volere svuotare il cestino? Questo rimuoverà tutte le risorse nel cestino in modo permanente da Immich.\nNon puoi annullare questa azione!", "enable": "Abilita", + "enable_backup": "Abilita Backup", "enable_biometric_auth_description": "Inserire il codice PIN per abilitare l'autenticazione biometrica", "enabled": "Abilitato", "end_date": "Data Fine", @@ -984,6 +1011,7 @@ "failed_to_load_assets": "Impossibile caricare gli asset", "failed_to_load_folder": "Impossibile caricare la cartella", "favorite": "Preferito", + "favorite_action_prompt": "{count} elementi aggiunti ai preferiti", "favorite_or_unfavorite_photo": "Aggiungi o rimuovi foto da preferiti", "favorites": "Preferiti", "favorites_page_no_favorites": "Nessun preferito", @@ -1127,6 +1155,7 @@ "library_page_sort_created": "Data di creazione", "library_page_sort_last_modified": "Ultima modifica", "library_page_sort_title": "Titolo album", + "licenses": "Licenze", "light": "Chiaro", "like_deleted": "Mi piace rimosso", "link_motion_video": "Collega video in movimento", @@ -1246,6 +1275,7 @@ "more": "Di piÚ", "move": "Sposta", "move_off_locked_folder": "Sposta al di fuori della cartella privata", + "move_to_lock_folder_action_prompt": "{count} elementi aggiunti alla cartella sicura", "move_to_locked_folder": "Sposta nella cartella privata", "move_to_locked_folder_confirmation": "Queste foto e video verranno rimossi da tutti gli album, e saranno visibili solo dalla cartella privata", "moved_to_archive": "Spostati {count, plural, one {# asset} other {# assets}} nell'archivio", @@ -1460,6 +1490,7 @@ "purchase_server_description_2": "Stato di Contributore", "purchase_server_title": "Server", "purchase_settings_server_activated": "La chiave del prodotto del server è gestita dall'amministratore", + "queue_status": "Messi in coda {count}/{total}", "rating": "Valutazione a stelle", "rating_clear": "Crea valutazione", "rating_count": "{count, plural, one {# stella} other {# stelle}}", @@ -1479,13 +1510,13 @@ "recently_taken_page_title": "Scattate di Recente", "refresh": "Aggiorna", "refresh_encoded_videos": "Ricarica video codificati", - "refresh_faces": "Aggiorna facce", + "refresh_faces": "Aggiorna volti", "refresh_metadata": "Ricarica metadati", "refresh_thumbnails": "Ricarica anteprime", "refreshed": "Aggiornato", "refreshes_every_file": "Rilegge tutti i file esistenti e nuovi", "refreshing_encoded_video": "Ricaricando il video codificato", - "refreshing_faces": "Aggiorna Facce", + "refreshing_faces": "Aggiornando volti", "refreshing_metadata": "Ricaricando i metadati", "regenerating_thumbnails": "Rigenerando le anteprime", "remove": "Rimuovi", @@ -1495,7 +1526,9 @@ "remove_custom_date_range": "Rimuovi intervallo data personalizzato", "remove_deleted_assets": "Rimuovi file offline", "remove_from_album": "Rimuovere dall'album", + "remove_from_album_action_prompt": "{count} elementi rimossi dall'album", "remove_from_favorites": "Rimuovi dai preferiti", + "remove_from_lock_folder_action_prompt": "{count} elementi rimossi dalla cartella sicura", "remove_from_locked_folder": "Rimuovi dalla cartella privata", "remove_from_locked_folder_confirmation": "Sei sicuro di voler spostare queste foto e questi video dalla cartella privata? Diventeranno visibili nella vostra libreria.", "remove_from_shared_link": "Rimuovi dal link condiviso", @@ -1667,6 +1700,7 @@ "settings_saved": "Impostazioni salvate", "setup_pin_code": "Configura un codice PIN", "share": "Condivisione", + "share_action_prompt": "Condivisi {count} elementi", "share_add_photos": "Aggiungi foto", "share_assets_selected": "{count} selezionati", "share_dialog_preparing": "Preparoâ€Ļ", @@ -1768,6 +1802,7 @@ "sort_title": "Titolo", "source": "Fonte", "stack": "Raggruppa", + "stack_action_prompt": "{count} elementi raggruppati", "stack_duplicates": "Raggruppa i duplicati", "stack_select_one_photo": "Seleziona una foto principale per il gruppo", "stack_selected_photos": "Impila foto selezionate", @@ -1838,6 +1873,7 @@ "total": "Totale", "total_usage": "Utilizzo totale", "trash": "Cestino", + "trash_action_prompt": "{count} elementi spostati nel cestino", "trash_all": "Cestina Tutto", "trash_count": "Cancella {count, number}", "trash_delete_asset": "Cestina/Cancella Asset", @@ -1855,6 +1891,7 @@ "unable_to_change_pin_code": "Impossibile cambiare il codice PIN", "unable_to_setup_pin_code": "Impossibile configurare il codice PIN", "unarchive": "Annulla l'archiviazione", + "unarchive_action_prompt": "{count} elementi rimossi dall'Archivio", "unarchived_count": "{count, plural, other {Non archiviati #}}", "undo": "Annulla", "unfavorite": "Rimuovi preferito", diff --git a/i18n/ja.json b/i18n/ja.json index 1649f978d8..69c0f368fa 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -427,6 +427,7 @@ "app_settings": "ã‚ĸプãƒĒč¨­åŽš", "appears_in": "これらãĢåĢぞれぞす", "archive": "ã‚ĸãƒŧã‚Ģイブ", + "archive_action_prompt": "ã‚ĸãƒŧã‚ĢイブãĢ{count}é …į›ŽčŋŊ加しぞした", "archive_or_unarchive_photo": "å†™įœŸã‚’ã‚ĸãƒŧã‚Ģイブぞたはã‚ĸãƒŧã‚Ģã‚¤ãƒ–č§Ŗé™¤", "archive_page_no_archived_assets": "ã‚ĸãƒŧã‚Ģã‚¤ãƒ–ã—ãŸå†™įœŸãžãŸã¯ãƒ“ãƒ‡ã‚Ēがありぞせん", "archive_page_title": "ã‚ĸãƒŧã‚Ģイブ ({count})", @@ -464,7 +465,6 @@ "assets": "ã‚ĸã‚ģット", "assets_added_count": "{count, plural, one {#個} other {#個}}ぎã‚ĸã‚ģットをčŋŊ加しぞした", "assets_added_to_album_count": "{count, plural, one {#個} other {#個}}ぎã‚ĸã‚ģットをã‚ĸãƒĢバムãĢčŋŊ加しぞした", - "assets_added_to_name_count": "{count, plural, one {#個} other {#個}}ぎã‚ĸã‚ģットを{hasName, select, true {{name}} other {新しいã‚ĸãƒĢバム}}ãĢčŋŊ加しぞした", "assets_cannot_be_added_to_album_count": "{count, plural, one {ã‚ĸã‚ģット} other {ã‚ĸã‚ģット}} はã‚ĸãƒĢバムãĢčŋŊ加できぞせん", "assets_count": "{count, plural, one {#個} other {#個}}ぎã‚ĸã‚ģット", "assets_deleted_permanently": "{count}é …į›Žã‚’åŽŒå…¨ãĢ削除しぞした", @@ -492,7 +492,7 @@ "background_location_permission_content": "æ­Ŗå¸¸ãĢWi-Fiぎ名前(SSID)ã‚’į˛åž—ã™ã‚‹ãĢはã‚ĸプãƒĒが常ãĢčŠŗį´°ãĒäŊįŊŽæƒ…å ąãĢã‚ĸクã‚ģ゚できるåŋ…čĻãŒã‚ã‚Šãžã™", "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": "選択されたã‚ĸãƒĢãƒãƒ ãŽå†™įœŸã¨å‹•į”ģぎ数", @@ -703,7 +703,7 @@ "daily_title_text_date": "MM DD, EE", "daily_title_text_date_year": "yyyy MM DD, EE", "dark": "ダãƒŧクãƒĸãƒŧド", - "darkTheme": "ダãƒŧクãƒĸãƒŧドを切りæ›ŋえ", + "dark_theme": "ダãƒŧクãƒĸãƒŧド切りæ›ŋえ", "date_after": "こぎæ—ĨäģĨ降", "date_and_time": "æ—Ĩäģ˜ã¨æ™‚é–“", "date_before": "こぎæ—ĨäģĨ前", @@ -719,6 +719,7 @@ "default_locale": "デフりãƒĢãƒˆãŽãƒ­ã‚ąãƒŧãƒĢ", "default_locale_description": "ブナã‚Ļã‚ļãŽãƒ­ã‚ąãƒŧãƒĢãĢåŸēãĨいãĻæ—Ĩäģ˜ã¨æ•°å€¤ã‚’フりãƒŧマットしぞす", "delete": "削除", + "delete_action_prompt": "{count}é …į›Žã‚’åŽŒå…¨ãĢ削除しぞした", "delete_album": "ã‚ĸãƒĢバムを削除", "delete_api_key_prompt": "æœŦåŊ“ãĢこぎAPI キãƒŧを削除しぞすか?", "delete_dialog_alert": "ã‚ĩãƒŧバãƒŧã¨ãƒ‡ãƒã‚¤ã‚šãŽä¸Ąæ–šã‹ã‚‰åŽŒå…¨ãĢ削除されぞす", @@ -799,6 +800,7 @@ "edit_key": "キãƒŧã‚’įˇ¨é›†", "edit_link": "ãƒĒãƒŗã‚¯ã‚’įˇ¨é›†ã™ã‚‹", "edit_location": "äŊįŊŽæƒ…å ąã‚’įˇ¨é›†", + "edit_location_action_prompt": "{count}é …į›ŽãŽäŊįŊŽæƒ…å ąã‚’å¤‰æ›´ã—ãžã—ãŸ", "edit_location_dialog_title": "äŊįŊŽæƒ…å ą", "edit_name": "名前を変更", "edit_people": "äēēį‰Šã‚’įˇ¨é›†", @@ -984,6 +986,7 @@ "failed_to_load_assets": "ã‚ĸã‚ģットぎロãƒŧドãĢå¤ąæ•—ã—ãžã—ãŸ", "failed_to_load_folder": "フりãƒĢダãƒŧぎčĒ­ãŋčžŧãŋãĢå¤ąæ•—", "favorite": "お気ãĢå…Ĩり", + "favorite_action_prompt": "{count}é …į›Žã‚’ãŠæ°—ãĢå…ĨりãĢčŋŊ加しぞした", "favorite_or_unfavorite_photo": "å†™įœŸã‚’ãŠæ°—ãĢいりãĢį™ģéŒ˛ãžãŸã¯č§Ŗé™¤", "favorites": "お気ãĢå…Ĩり", "favorites_page_no_favorites": "お気ãĢå…Ĩりį™ģéŒ˛ã•ã‚ŒãŸé …į›ŽãŒã‚ã‚Šãžã›ã‚“", @@ -1246,6 +1249,7 @@ "more": "ã‚‚ãŖã¨čĄ¨į¤ē", "move": "į§ģ動", "move_off_locked_folder": "éĩäģ˜ããƒ•りãƒĢダãƒŧからå‡ēす", + "move_to_lock_folder_action_prompt": "{count}é …į›Žã‚’éĩäģ˜ããƒ•りãƒĢダãƒŧãĢčŋŊ加しぞした", "move_to_locked_folder": "éĩäģ˜ããƒ•りãƒĢダãƒŧへį§ģ動", "move_to_locked_folder_confirmation": "ã“ã‚Œã‚‰ãŽå†™įœŸã‚„å‹•į”ģはすずãĻぎã‚ĸãƒĢバムから外され、éĩäģ˜ããƒ•りãƒĢダãƒŧ内でぎãŋ閲čĻ§å¯čƒŊãĢãĒりぞす", "moved_to_archive": "{count, plural, one {#} other {#}}é …į›Žã‚’ã‚ĸãƒŧã‚Ģイブしぞした", @@ -1496,6 +1500,7 @@ "remove_deleted_assets": "ã‚Ēãƒ•ãƒŠã‚¤ãƒŗãŽã‚ĸã‚ģットを削除", "remove_from_album": "ã‚ĸãƒĢバムから削除", "remove_from_favorites": "お気ãĢå…Ĩã‚Šč§Ŗé™¤", + "remove_from_lock_folder_action_prompt": "{count}é …į›Žã‚’éĩäģ˜ããƒ•りãƒĢダãƒŧからå‡ēしぞした", "remove_from_locked_folder": "éĩäģ˜ããƒ•りãƒĢダãƒŧから取り除く", "remove_from_locked_folder_confirmation": "é¸æŠžã—ãŸå†™įœŸãƒģ動į”ģをéĩäģ˜ããƒ•りãƒĢダãƒŧぎ外ãĢå‡ēしãĻよろしいですかīŧŸãƒŠã‚¤ãƒ–ナãƒĒãĢå†ãŗčĄ¨į¤ēされるようãĢãĒりぞす", "remove_from_shared_link": "å…ąæœ‰ãƒĒãƒŗã‚¯ã‹ã‚‰å‰Šé™¤", @@ -1838,6 +1843,7 @@ "total": "合荈", "total_usage": "įˇäŊŋį”¨é‡", "trash": "ã‚´ãƒŸįŽą", + "trash_action_prompt": "{count}é …į›Žã‚’ã‚´ãƒŸįŽąãĢį§ģ動しぞした", "trash_all": "全ãĻ削除", "trash_count": "{count, number}æžšã‚´ãƒŸįŽąã¸į§ģ動", "trash_delete_asset": "ã‚ĸã‚ģãƒƒãƒˆã‚’ã‚´ãƒŸįŽąã¸į§ģ動/削除", diff --git a/i18n/ko.json b/i18n/ko.json index 01b29e6776..1890962e5a 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -22,6 +22,7 @@ "add_partner": "파트너 ėļ”ę°€", "add_path": "ę˛Ŋ로 ėļ”ę°€", "add_photos": "ė‚Ŧė§„ ėļ”ę°€", + "add_tag": "태그 ėļ”ę°€í•˜ę¸°", "add_to": "ė•¨ë˛”ė— ėļ”ę°€â€Ļ", "add_to_album": "ė•¨ë˛”ė— ėļ”ę°€", "add_to_album_bottom_sheet_added": "{album}뗐 ėļ”ę°€ë˜ė—ˆėŠĩ니다.", @@ -33,6 +34,7 @@ "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 및 기타 ė¸ėĻ 네렕 관ëĻŦ", @@ -43,7 +45,7 @@ "backup_database_enable_description": "ë°ė´í„°ë˛ ė´ėŠ¤ 덤프 í™œė„ąí™”", "backup_keep_last_amount": "ëŗ´ę´€í•  ė´ė „ ë¤í”„ė˜ 눘", "backup_settings": "ë°ė´í„°ë˛ ė´ėŠ¤ 덤프 네렕", - "backup_settings_description": "ë°ė´í„°ë˛ ė´ėŠ¤ 덤프 ė„¤ė •ė„ 관ëĻŦ합니다. 및溠: ė´ ėž‘ė—…ė€ ė§„í–‰ 및 ė‹¤íŒ¨ ė—Ŧëļ€ëĨŧ í™•ė¸í•  눘 ė—†ėŠĩ니다.", + "backup_settings_description": "ë°ė´í„°ë˛ ė´ėŠ¤ 덤프 ė„¤ė •ė„ 관ëĻŦ합니다.", "cleared_jobs": "ėž‘ė—… ė¤‘ë‹¨: {job}", "config_set_by_file": "현ėžŦ ęĩŦė„ąė€ 네렕 파ėŧė„ í†ĩ해 ė§€ė •ë˜ė–´ ėžˆėŠĩ니다.", "confirm_delete_library": "{library} ëŧė´ë¸ŒëŸŦëĻŦëĨŧ ė‚­ė œí•˜ė‹œę˛ ėŠĩ니까?", @@ -164,12 +166,26 @@ "metadata_settings_description": "ëŠ”íƒ€ë°ė´í„° 네렕 관ëĻŦ", "migration_job": "ë§ˆė´ęˇ¸ë ˆė´ė…˜", "migration_job_description": "각 항ëĒŠė˜ ė„Ŧ네ėŧ 및 ė¸ëŦŧė˜ ė–ŧęĩ´ė„ ėĩœė‹  폴더 ęĩŦėĄ°ëĄœ ë§ˆė´ęˇ¸ë ˆė´ė…˜", + "nightly_tasks_cluster_faces_setting_description": "ėƒˆëĄœ ę°ė§€ëœ ė–ŧęĩ´ė— 대하ė—Ŧ ė–ŧęĩ´ ė¸ė‹ė„ ė‹¤í–‰", + "nightly_tasks_cluster_new_faces_setting": "냈 ė–ŧęĩ´ ëŦļ기", + "nightly_tasks_database_cleanup_setting": "ë°ė´í„°ë˛ ė´ėŠ¤ ė •ëĻŦ ėž‘ė—…", + "nightly_tasks_database_cleanup_setting_description": "ë°ė´í„°ë˛ ė´ėŠ¤ė—ė„œ ė˜¤ëž˜ë˜ęą°ë‚˜ ë§ŒëŖŒëœ ë°ė´í„° ė •ëĻŦ하기", + "nightly_tasks_generate_memories_setting": "메ëǍëĻŦ ėƒė„ąí•˜ę¸°", + "nightly_tasks_generate_memories_setting_description": "항ëĒŠė—ė„œ ėƒˆëĄœėš´ 메ëǍëĻŦ 만들기", + "nightly_tasks_missing_thumbnails_setting": "누ëŊ된 ė„Ŧ네ėŧ ėƒė„ąí•˜ę¸°", + "nightly_tasks_missing_thumbnails_setting_description": "ė„Ŧ네ėŧė´ ė—†ëŠ” 항ëĒŠė— 대해 ė„Ŧ네ėŧ ėƒė„ą ëŒ€ę¸°ė—´ė— ėļ”ę°€í•˜ę¸°", + "nightly_tasks_settings": "ė•ŧ간 ėž‘ė—… 네렕", + "nightly_tasks_settings_description": "ė•ŧ간 ėž‘ė—… 관ëĻŦ", + "nightly_tasks_start_time_setting": "ė‹œėž‘ ė‹œę°„", + "nightly_tasks_start_time_setting_description": "ė„œë˛„ę°€ ė•ŧ간 ėž‘ė—…ė„ ė‹œėž‘í•  ė‹œę°„", + "nightly_tasks_sync_quota_usage_setting": "ė‚ŦėšŠëŸ‰ 동기화", + "nightly_tasks_sync_quota_usage_setting_description": "현ėžŦ ė‚ŦėšŠëŸ‰ė— 따ëŧ ė‚ŦėšŠėžė˜ ė‚ŦėšŠëŸ‰ ę°ąė‹ í•˜ę¸°", "no_paths_added": "ėļ”ę°€ëœ ę˛Ŋ로 ė—†ėŒ", "no_pattern_added": "ėļ”ę°€ëœ ęˇœėš™ ė—†ėŒ", "note_apply_storage_label_previous_assets": "및溠: ė´ė „ė— ė—…ëĄœë“œí•œ 항ëĒŠė—ë„ ėŠ¤í† ëĻŦė§€ ë ˆė´ë¸”ė„ ė ėšŠí•˜ë ¤ëŠ´ ë‹¤ėŒė„ ė‹¤í–‰í•Šë‹ˆë‹¤,", "note_cannot_be_changed_later": "ėŖŧė˜: ėļ”후 ëŗ€ę˛Ŋ할 눘 ė—†ėŠĩ니다!", "notification_email_from_address": "ëŗ´ë‚¸ ė‚Ŧ람 ė´ëŠ”ėŧ", - "notification_email_from_address_description": "ëŗ´ë‚¸ ė‚ŦëžŒė˜ ė´ëŠ”ėŧ ėŖŧė†Œ, 똈: \"Immich Photo Server \"", + "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 ė¸ėĻė„œ ėœ íš¨ė„ą 검ė‚Ŧ 똤ëĨ˜ ëŦ´ė‹œ (ęļŒėžĨë˜ė§€ ė•ŠėŒ)", @@ -194,6 +210,7 @@ "oauth_mobile_redirect_uri": "ëĒ¨ë°”ėŧ ëĻŦë‹¤ė´ë ‰íŠ¸ URI", "oauth_mobile_redirect_uri_override": "ëĒ¨ë°”ėŧ ëĻŦë‹¤ė´ë ‰íŠ¸ URI ėžŦė •ė˜", "oauth_mobile_redirect_uri_override_description": "OAuth ęŗĩę¸‰ėžę°€ ''{callback}''ęŗŧ ę°™ė€ ëĒ¨ë°”ėŧ URIëĨŧ 렜ęŗĩí•˜ė§€ ė•ŠëŠ” ę˛Ŋ뚰 í™œė„ąí™”í•˜ė„¸ėš”.", + "oauth_role_claim": "ė—­í•  ėˆ˜ë š", "oauth_settings": "OAuth", "oauth_settings_description": "OAuth ëĄœęˇ¸ė¸ 네렕 관ëĻŦ", "oauth_settings_more_details": "ė´ 기ëŠĨ뗐 대한 ėžė„¸í•œ ë‚´ėšŠė€ ëŦ¸ė„œëĨŧ ė°¸ėĄ°í•˜ė„¸ėš”.", @@ -202,7 +219,7 @@ "oauth_storage_quota_claim": "ėŠ¤í† ëĻŦė§€ 할당량 ė„ íƒ", "oauth_storage_quota_claim_description": "ėŠ¤í† ëĻŦė§€ í• ë‹šëŸ‰ė„ ė‚ŦėšŠėžę°€ ėž…ë Ĩ한 값ėœŧ로 ėžë™ ė„¤ė •í•Šë‹ˆë‹¤.", "oauth_storage_quota_default": "ėŠ¤í† ëĻŦė§€ 할당량 ę¸°ëŗ¸ę°’ (GiB)", - "oauth_storage_quota_default_description": "ėž…ë Ĩí•˜ė§€ ė•Šė€ ę˛Ŋ뚰 ė‚ŦėšŠí•  GiB ë‹¨ėœ„ė˜ ę¸°ëŗ¸ 할당량 (ëŦ´ė œí•œ í• ë‹šëŸ‰ė˜ ę˛Ŋ뚰 0 ėž…ë Ĩ)", + "oauth_storage_quota_default_description": "ėž…ë Ĩí•˜ė§€ ė•Šė€ ę˛Ŋ뚰 ė‚ŦėšŠí•  GiB ë‹¨ėœ„ė˜ ę¸°ëŗ¸ 할당량", "oauth_timeout": "ėš”ė˛­ íƒ€ėž„ė•„ė›ƒ", "oauth_timeout_description": "ėš”ė˛­ íƒ€ėž„ė•„ė›ƒ (밀ëĻŦ봈 ë‹¨ėœ„)", "password_enable_description": "ė´ëŠ”ėŧęŗŧ 비밀번호로 ëĄœęˇ¸ė¸", @@ -242,6 +259,7 @@ "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": "ė—…ëĄœë“œëœ 항ëĒŠė˜ 폴더 ęĩŦėĄ° 및 파ėŧ ė´ëĻ„ 관ëĻŦ", @@ -288,7 +306,7 @@ "transcoding_encoding_options": "ė¸ėŊ”딊 ė˜ĩė…˜", "transcoding_encoding_options_description": "ė¸ėŊ”딊된 ë™ė˜ėƒė˜ ėŊ”덱, í•´ėƒë„, í’ˆė§ˆ 및 기타 ė˜ĩė…˜ 네렕", "transcoding_hardware_acceleration": "í•˜ë“œė›¨ė–´ ę°€ė†", - "transcoding_hardware_acceleration_description": "ė‹¤í—˜ė ė¸ 기ëŠĨėž…ë‹ˆë‹¤. ė†ë„ę°€ í–Ĩėƒë˜ė§€ë§Œ 동ėŧ ëš„íŠ¸ë ˆė´íŠ¸ė—ė„œ í’ˆė§ˆė´ ėƒëŒ€ė ėœŧ로 ë‚Žė„ 눘 ėžˆėŠĩ니다.", + "transcoding_hardware_acceleration_description": "ė‹¤í—˜ė ė¸ 기ëŠĨėž…ë‹ˆë‹¤. íŠ¸ëžœėŠ¤ėŊ”ë”Šė´ 뚨ëŧė§€ė§€ë§Œ 동ėŧ ëš„íŠ¸ë ˆė´íŠ¸ė—ė„œ í’ˆė§ˆė´ ėƒëŒ€ė ėœŧ로 ë‚Žė„ 눘 ėžˆėŠĩ니다.", "transcoding_hardware_decoding": "í•˜ë“œė›¨ė–´ 디ėŊ”딊", "transcoding_hardware_decoding_setting_description": "ė¸ėŊ”딊 ę°€ė†ė„ ėœ„í•´ ė—”ë“œ íˆŦ ė—”ë“œ ę°€ė†ė„ ė‚ŦėšŠí•Šë‹ˆë‹¤. ëĒ¨ë“  ë™ė˜ėƒė—ė„œ ėž‘ë™í•˜ė§€ ė•Šė„ 눘 ėžˆėŠĩ니다.", "transcoding_max_b_frames": "ėĩœëŒ€ B í”„ë ˆėž„", @@ -354,10 +372,12 @@ "admin_password": "관ëĻŦėž 비밀번호", "administration": "관ëĻŦ", "advanced": "溠揉", + "advanced_settings_beta_timeline_subtitle": "ėƒˆëĄœėš´ ė•ą ę˛Ŋ험 ė‚ŦėšŠí•˜ę¸°", + "advanced_settings_beta_timeline_title": "베타 íƒ€ėž„ëŧė¸", "advanced_settings_enable_alternate_media_filter_subtitle": "ė´ ė˜ĩė…˜ė„ ė‚ŦėšŠí•˜ëŠ´ 동기화 뤑 ë¯¸ë””ė–´ëĨŧ ëŒ€ė˛´ 揰뤀ėœŧ로 필터링할 눘 ėžˆėŠĩ니다. ė•ąė´ ëĒ¨ë“  ė•¨ë˛”ė„ ė œëŒ€ëĄœ ę°ė§€í•˜ė§€ ëĒģ할 때만 ė‚ŦėšŠí•˜ė„¸ėš”.", "advanced_settings_enable_alternate_media_filter_title": "ëŒ€ė˛´ 기기 ė•¨ë˛” 동기화 필터 ė‚ŦėšŠ (ė‹¤í—˜ė )", "advanced_settings_log_level_title": "로그 레벨: {level}", - "advanced_settings_prefer_remote_subtitle": "ėŧëļ€ ę¸°ę¸°ė˜ ę˛Ŋ뚰 기기 ë‚´ė˜ ė„Ŧ네ėŧė„ 로드하는 ė†ë„ę°€ ë§¤ėš° 느ëĻŊ니다. ė„œë˛„ ė´ë¯¸ė§€ëĨŧ ëŒ€ė‹  로드하려면 ė´ ė„¤ė •ė„ í™œė„ąí™”í•˜ė„¸ėš”.", + "advanced_settings_prefer_remote_subtitle": "ėŧëļ€ ę¸°ę¸°ė˜ ę˛Ŋ뚰 로ėģŦ 항ëĒŠė—ė„œ ė„Ŧ네ėŧė„ 로드하는 ė†ë„ę°€ ë§¤ėš° 느ëĻŊ니다. ė„œë˛„ ė´ë¯¸ė§€ëĨŧ ëŒ€ė‹  로드하려면 ė´ ė„¤ė •ė„ í™œė„ąí™”í•˜ė„¸ėš”.", "advanced_settings_prefer_remote_title": "ė„œë˛„ ė´ë¯¸ė§€ ė„ í˜¸", "advanced_settings_proxy_headers_subtitle": "ë„¤íŠ¸ė›ŒíŦ ėš”ė˛­ė„ ëŗ´ë‚ŧ 때 Immich가 ė‚ŦėšŠí•  í”„ëĄė‹œ 헤더ëĨŧ ė •ė˜í•Šë‹ˆë‹¤.", "advanced_settings_proxy_headers_title": "í”„ëĄė‹œ 헤더", @@ -401,6 +421,9 @@ "album_with_link_access": "링íŦ가 ėžˆëŠ” ę˛Ŋ뚰 누ęĩŦ나 ė´ ė•¨ë˛”ė˜ ė‚Ŧė§„ęŗŧ ė¸ëŦŧė„ ëŗŧ 눘 ėžˆėŠĩ니다.", "albums": "ė•¨ë˛”", "albums_count": "ė•¨ë˛” {count, plural, one {{count, number}氜} other {{count, number}氜}}", + "albums_default_sort_order": "ę¸°ëŗ¸ ė•¨ë˛” ė •ë Ŧ ėˆœė„œ", + "albums_default_sort_order_description": "냈 ė•¨ë˛”ė„ ėƒė„ąí•  때 ę¸°ëŗ¸ė ėœŧ로 항ëĒŠė„ ė •ë Ŧ할 ėˆœė„œ.", + "albums_feature_description": "다ëĨ¸ ė‚ŦėšŠėžė™€ ęŗĩėœ í•  눘 ėžˆëŠ” 항ëĒŠ ëĒ¨ėŒ.", "all": "ëĒ¨ë‘", "all_albums": "ëĒ¨ë“  ė•¨ë˛”", "all_people": "ëĒ¨ë“  ė¸ëŦŧ", @@ -421,6 +444,7 @@ "app_settings": "ė•ą 네렕", "appears_in": "ë‹¤ėŒ ė•¨ë˛”ė— íŦ함됨", "archive": "ëŗ´ę´€í•¨", + "archive_action_prompt": "ëŗ´ę´€í•¨ė— {count}개가 ėļ”ę°€ë˜ė—ˆėŠĩ니다.", "archive_or_unarchive_photo": "ëŗ´ę´€ 래ëĻŦ 또는 í•´ė œ", "archive_page_no_archived_assets": "ëŗ´ę´€ëœ 항ëĒŠ ė—†ėŒ", "archive_page_title": "ëŗ´ę´€í•¨ ({count})", @@ -458,10 +482,12 @@ "assets": "항ëĒŠ", "assets_added_count": "{count, plural, one {#氜} other {#氜}} 항ëĒŠ ėļ”가됨", "assets_added_to_album_count": "ė•¨ë˛”ė— 항ëĒŠ {count, plural, one {#氜} other {#氜}} ėļ”가됨", - "assets_added_to_name_count": "{hasName, select, true {{name}} other {냈 ė•¨ë˛”}}뗐 항ëĒŠ {count, plural, one {#氜} other {#氜}} ėļ”가됨", + "assets_cannot_be_added_to_album_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 {#氜}}ëĨŧ ė œęą°í–ˆėŠĩ니다.", @@ -476,6 +502,7 @@ "authorized_devices": "ė¸ėĻëœ 기기", "automatic_endpoint_switching_subtitle": "ė§€ė •ëœ Wi-Fi가 ė‚ŦėšŠ 가ëŠĨ한 ę˛Ŋ뚰 내ëļ€ë§ė„ í†ĩ해 ė—°ę˛°í•˜ęŗ , ęˇ¸ë ‡ė§€ ė•Šėœŧ늴 다ëĨ¸ 뗰枰 ë°Šė‹ė„ ė‚ŦėšŠí•Šë‹ˆë‹¤.", "automatic_endpoint_switching_title": "ėžë™ URL ė „í™˜", + "autoplay_slideshow": "ėŠŦëŧė´ë“œ ė‡ŧ ėžë™ ėžŦėƒ", "back": "뒤로", "back_close_deselect": "뒤로, ë‹Ģ기, ė„ íƒ ėˇ¨ė†Œ", "background_location_permission": "밹꡸ëŧėš´ë“œ ėœ„ėš˜ ęļŒí•œ", diff --git a/i18n/lt.json b/i18n/lt.json index d5e6ff20ed..14668921d2 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -14,6 +14,7 @@ "add_a_location": "Pridėti vietovę", "add_a_name": "Pridėti vardą", "add_a_title": "Pridėti pavadinimą", + "add_endpoint": "Pridėti galutinį taÅĄką", "add_exclusion_pattern": "Pridėti iÅĄimčiÅŗ ÅĄabloną", "add_import_path": "Pridėti importavimo kelią", "add_location": "Pridėti vietovę", @@ -32,6 +33,8 @@ "added_to_favorites": "Pridėta prie mėgstamiausiÅŗ", "added_to_favorites_count": "{count, plural, one {# pridėtas} few {# pridėti} other {# pridėta}} prie mėgstamiausiÅŗ", "admin": { + "add_exclusion_pattern_description": "Pridėti iÅĄimčiÅŗ taisyklęs. Plaikomi simboliai *,**, ir ?. Ignoruoti bet kokius failus bet kuriame aplanke uÅžvadintame \"Raw\", naudokite \"**/RAW/**\". Ignoravimui failÅŗ su plėtiniu \".tif\", naudokite \"**/*.tiff\". Aplanko kelio nustatymams, naudokite \"/aplanko/kelias/ignoruoti/**\"", + "admin_user": "Administratorius", "asset_offline_description": "Å is iÅĄorinės bibliotekos elementas nebepasiekiamas diske ir buvo perkeltas į ÅĄiukÅĄliadėŞę. Jei failas buvo perkeltas toje pačioje bibliotekoje, laiko skalėje rasite naują atitinkamą elementą. Jei norite ÅĄÄ¯ elementą atkurti, įsitikinkite, kad Immich gali pasiekti failą Åžemiau nurodytu adresu, ir suvykdykite bibliotekos skenavimą.", "authentication_settings": "Autentifikavimo nustatymai", "authentication_settings_description": "Tvarkyti slaptaÅžodÅžiÅŗ, OAuth ir kitus autentifikavimo nustatymus", @@ -42,8 +45,8 @@ "backup_database_enable_description": "ÄŽgalinti duomenÅŗ bazės iÅĄklotinės", "backup_keep_last_amount": "IÅĄsaugomÅŗ ankstesniÅŗ duomenÅŗ bazės iÅĄklotiniÅŗ skaičius", "backup_settings": "DuomenÅŗ bazės iÅĄklotiniÅŗ nustatymai", - "backup_settings_description": "Tvarkyti duomenÅŗ bazės iÅĄklotinės nustatymus. Pastaba: Å ie darbai nėra stebimi ir jums nebus praneÅĄta apie nesėkmę.", - "cleared_jobs": "IÅĄvalyti darbai: {job}", + "backup_settings_description": "Tvarkyti duomenÅŗ bazės iÅĄklotinės nustatymus. Pastaba: ÅĄie darbai nėra stebimi ir jums nebus praneÅĄta apie nesėkmę.", + "cleared_jobs": "IÅĄvalytos uÅžduotys uÅžduočiai: {job}", "config_set_by_file": "KonfigÅĢracija nustatyta pagal konfigÅĢracinį failą", "confirm_delete_library": "Ar tikrai norite iÅĄtrinti {library} biblioteką?", "confirm_delete_library_assets": "Ar tikrai norite iÅĄtrinti ÅĄią biblioteką? Å is veiksmas iÅĄtrins {count, plural, one {# contained asset} other {all # contained assets}} iÅĄ Immich ir negali bÅĢti grÄ…Åžintas. Failai liks diske.", @@ -51,7 +54,7 @@ "confirm_reprocess_all_faces": "Ar tikrai norite iÅĄ naujo apdoroti visus veidus? Tai taip pat iÅĄtrins įvardytus asmenis.", "confirm_user_password_reset": "Ar tikrai norite iÅĄ naujo nustatyti {user} slaptaÅžodį?", "confirm_user_pin_code_reset": "Ar tikrai norite iÅĄ naujo nustatyti {user} PIN kodą?", - "create_job": "Sukurti darbą", + "create_job": "Sukurti uÅžduotį", "cron_expression": "Cron iÅĄraiÅĄka", "cron_expression_description": "Nustatyti skenavimo intervalą naudojant cron formatą. Norėdami gauti daugiau informacijos ÅžiÅĢrėkite Crontab Guru", "cron_expression_presets": "IÅĄankstiniai Cron nustatymai", @@ -62,15 +65,19 @@ "face_detection": "VeidÅŗ aptikimas", "face_detection_description": "VeidÅŗ aptikimas bibliotekos elementuose naudojant maÅĄininį mokymąsi. Vaizdo įraÅĄÅŗ atveju naudojama tik miniatiÅĢra. \"Atnaujinti\" iÅĄ naujo nuskaito visus bibliotekos elementus. \"Atstatyti\" ne tik atnaujina, bet ir iÅĄvalo visus esamus veidÅŗ duomenis. \"TrÅĢkstami\" nuskaito tik dar nenuskaitytus bibliotekos elementus. VeidÅŗ aptikimo darbui pasibaigus, aptikti veidai patenka į veidÅŗ atpaÅžinimo darbÅŗ eilę, kur jie priskiriami jau esamiems ar naujai atpaÅžintiems Åžmonėms.", "facial_recognition_job_description": "AptiktÅŗ veidÅŗ atpaÅžinimas ir priskyrimas Åžmonėms. Å is darbas vykdomas pasibaigus \"veidÅŗ aptikimo\" darbui. \"Atstatyti\" (per)grupuoja visus aptiktus veidus. \"TrÅĢkstami\" apdoroja jokiam Åžmogui dar nepriskirtus aptiktus veidus.", - "failed_job_command": "Darbo {job} komanda {command} nepavyko", + "failed_job_command": "UÅžduoties {job} komanda {command} nepavyko", "force_delete_user_warning": "ÄŽSPĖJIMAS: Å is veiksmas iÅĄ karto paÅĄalins naudotoją ir visą jo informaciją. Å is Åžingsnis nesugrÄ…Åžinamas ir failÅŗ nebus galima atkurti.", "image_format": "Formatas", "image_format_description": "WebP sukuria maÅžesnius failus nei JPEG, tačiau lėčiau juos apdoroja.", + "image_fullsize_description": "Pilno dydÅžio nuotrauka be meta duomenÅŗ naudojama priartinus", "image_fullsize_enabled": "ÄŽgalinti pilno dydÅžio nuotraukÅŗ generavimą", + "image_fullsize_enabled_description": "Generuoti viso dydÅžio vaizdą neinternetui pritaikytiems formatams. Kai įjungta parinktis „Pirmenybė įterptai perÅžiÅĢrai“, įterptosios perÅžiÅĢros naudojamos tiesiogiai be konvertavimo. Tai neturi įtakos internetui pritaikytiems formatams, pvz., JPEG", "image_fullsize_quality_description": "Pilno dydÅžio nuotraukÅŗ kokybė 1-100. Didesnė yra geresnė, tačiau sukuria didesniu failus.", "image_fullsize_title": "Pilno dydÅžio nuotraukÅŗ Nustatymai", "image_prefer_embedded_preview": "Pageidautinai rodyti įterptą perÅžiÅĢrą", + "image_prefer_embedded_preview_setting_description": "Naudokite įterptąsias perÅžiÅĢras RAW nuotraukose kaip įvestį vaizdÅŗ apdorojimui ir, jei įmanoma, tai gali suteikti tikslesnes kai kuriÅŗ vaizdÅŗ spalvas, tačiau perÅžiÅĢros kokybė priklauso nuo fotoaparato, todėl vaizde gali bÅĢti daugiau glaudinimo artefaktÅŗ.", "image_prefer_wide_gamut": "Teikti pirmenybę plačiai gamai", + "image_prefer_wide_gamut_setting_description": "MiniatiÅĢroms naudokite „Display P3“. Taip geriau iÅĄsaugomas vaizdÅŗ, turinčiÅŗ plačias spalvÅŗ erdves, ryÅĄkumas, tačiau senesniuose įrenginiuose su senesne narÅĄyklės versija vaizdai gali atrodyti kitaip. sRGB vaizdai iÅĄsaugomi kaip sRGB, kad bÅĢtÅŗ iÅĄvengta spalvÅŗ pasikeitimo.", "image_preview_description": "Vidutinio dydÅžio vaizdas su iÅĄvalytais metaduomenimis, naudojamas kai ÅžiÅĢrimas vienas objektas arba maÅĄininiam mokymuisi", "image_preview_quality_description": "PerÅžiÅĢros kokybė nuo 1-100. AukÅĄtesnės reikÅĄmės yra geriau, bet sukuriami didesni failai gali sumaÅžinti programos reagavimo laiką. MaÅžos vertės nustatymas gali paveikti maÅĄininio mokymo kokybę.", "image_preview_title": "PerÅžiÅĢros nustatymai", @@ -83,11 +90,11 @@ "image_thumbnail_quality_description": "MiniatiÅĢros kokybė nuo 1-100. AukÅĄtesnės reikÅĄmės yra geriau, bet pagaminami didesni failai ir gali bÅĢti sulėtintas programos reagavimo greitis.", "image_thumbnail_title": "MiniatiÅĢros nustatymai", "job_concurrency": "{job} lygiagretumas", - "job_created": "Darbas sukurtas", - "job_not_concurrency_safe": "Å is darbas nėra saugus apdoroti lygiagrečiai.", - "job_settings": "DarbÅŗ nustatymai", - "job_settings_description": "Keisti darbÅŗ lygiagretumą", - "job_status": "DarbÅŗ bÅĢsenos", + "job_created": "UÅžduotis sukurta", + "job_not_concurrency_safe": "Å i uÅžduotis nėra saugi apdoroti lygiagrečiai.", + "job_settings": "UÅžduočiÅŗ nustatymai", + "job_settings_description": "Keisti uÅžduočiÅŗ lygiagretumą", + "job_status": "UÅžduočiÅŗ bÅĢsenos", "library_created": "Sukurta biblioteka: {library}", "library_deleted": "Biblioteka iÅĄtrinta", "library_import_path_description": "Nurodykite aplanką, kurį norite importuoti. Å iame aplanke, įskaitant poaplankius, bus nuskaityti vaizdai ir vaizdo įraÅĄai.", @@ -104,6 +111,7 @@ "logging_level_description": "ÄŽjungus, kokį Åžurnalo vedimo lygį naudot.", "logging_settings": "ÅŊurnalo vedimas", "machine_learning_clip_model": "CLIP modelis", + "machine_learning_clip_model_description": "Pavadinimas CLIP modelio įvardintio here. Dėmesio, keičiant modelį jÅĢs privalote iÅĄ naujo paleisti 'IÅĄmaniosios PaieÅĄkos' uÅžduotį visiems vaizdams.", "machine_learning_duplicate_detection": "DublikatÅŗ aptikimas", "machine_learning_duplicate_detection_enabled": "ÄŽjungti dublikatÅŗ aptikimą", "machine_learning_duplicate_detection_enabled_description": "Jei iÅĄjungta, visiÅĄkai identiÅĄki elementai vis tiek bus deduplikuoti.", @@ -113,12 +121,15 @@ "machine_learning_facial_recognition": "VeidÅŗ atpaÅžinimas", "machine_learning_facial_recognition_description": "Aptikti, atpaÅžinti ir sugrupuoti veidus nuotraukose", "machine_learning_facial_recognition_model": "VeidÅŗ atpaÅžinimo modelis", + "machine_learning_facial_recognition_model_description": "Modeliai iÅĄvardinti apimties maŞėjančia tvarka. Didieji modeliai yra lėti ir naudoja daugiau atminties, tačiau sukuria geresnius rezultatus. Pastebime kad keičiant modelį jÅĢs turite iÅĄ naujo paleisti VeidÅŗ AtpaÅžinimo uÅžduotį visiems vaizdams.", "machine_learning_facial_recognition_setting": "ÄŽgalinti veidÅŗ atpaÅžinimą", "machine_learning_facial_recognition_setting_description": "IÅĄjungus, vaizdai nebus uÅžÅĄifruoti veidÅŗ atpaÅžinimui ir nebus naudojami ÅŊmoniÅŗ sekcijoje NarÅĄymo puslapyje.", "machine_learning_max_detection_distance": "Maksimalus aptikimo atstumas", "machine_learning_max_detection_distance_description": "DidÅžiausias atstumas tarp dviejÅŗ vaizdÅŗ, kad jie bÅĢtÅŗ laikomi dublikatais, svyruoja nuo 0,001 iki 0,1. Didesnės vertės aptiks daugiau dublikatÅŗ, tačiau gali bÅĢti klaidingai teigiami.", "machine_learning_max_recognition_distance": "Maksimalus atpaÅžinimo atstumas", + "machine_learning_max_recognition_distance_description": "DidÅžiausias skirtumas tarp veidÅŗ, kad bÅĢtÅŗ uÅžskaityti kaip vienas ir tas pats asmuo, rÄ—Åžis nuo 0-2. MaÅžinant tai gali apsaugoti nuo dviejÅŗ ÅžmoniÅŗ Åžymėjimo tuo pačiu asmeniu, didinant tai gali apsaugoti nuo to pačio asmens Åžymėjimo kaip du skirtingus Åžmones. Pastebime kad yra paprasčiau apjungti keliÅŗ ÅžmoniÅŗ modelius į vieną nei vieną iÅĄdalinti į du, taigi kai įmanoma geriau naudoti maÅžensę ribą.", "machine_learning_min_detection_score": "Minimalus aptikimo balas", + "machine_learning_min_detection_score_description": "Minimalus uÅžtikrintumo balas veido aptikimui nuo 0-1. MaÅžesnė reikÅĄmė aptiks daugiau veidÅŗ tačiau bus ir daugiau klaidingÅŗ teigiamÅŗ reÅžultatÅŗ.", "machine_learning_min_recognized_faces": "MaÅžiausias atpaÅžintÅŗ veidÅŗ skaičius", "machine_learning_min_recognized_faces_description": "MaÅžiausias atpaÅžintÅŗ veidÅŗ skaičius asmeniui, kurį reikia sukurti. Tai padidinus, veido atpaÅžinimas tampa tikslesnis, bet padidėja tikimybė, kad veidas Åžmogui nepriskirtas.", "machine_learning_settings": "MaÅĄininio mokymosi nustatymai", @@ -151,10 +162,14 @@ "metadata_faces_import_setting_description": "Importuoti veidus iÅĄ vaizdo EXIF duomenÅŗ ir papildomÅŗ failÅŗ", "metadata_settings": "MetaduomenÅŗ nustatymai", "metadata_settings_description": "Tvarkyti metaduomenÅŗ nustatymus", - "migration_job": "Migracija", + "migration_job": "Tvarkymas", + "migration_job_description": "Pertvarkytį turinio ir veidÅŗ miniatiÅĢras pagal naują struktÅĢrą", + "nightly_tasks_cluster_faces_setting_description": "Paleisti veido atpaÅžinimą naujai aptiktiems veidams", + "nightly_tasks_cluster_new_faces_setting": "Sugrupuoti naujus veidus", + "nightly_tasks_database_cleanup_setting": "DuomenÅŗ bazės valymo darbai", "no_paths_added": "Keliai nepridėti", "no_pattern_added": "Å ablonas nepridėtas", - "note_apply_storage_label_previous_assets": "Pastaba: norėdami pritaikyti saugyklos etiketę seniau įkeltiems iÅĄtekliams, paleiskite", + "note_apply_storage_label_previous_assets": "Pastaba: norėdami pritaikyti Saugyklos ÅŊymą seniau įkeltiems iÅĄtekliams, paleiskite", "note_cannot_be_changed_later": "PASTABA: Vėliau to pakeisti negalima!", "notification_email_from_address": "IÅĄ adreso", "notification_email_from_address_description": "Siuntėjo elektroninis adresas, pavyzdÅžiui: \"Immich Photo Server \"", @@ -177,20 +192,29 @@ "oauth_auto_register": "Automatinis registravimas", "oauth_auto_register_description": "AutomatiÅĄkai uÅžregistruoti naujus naudotojus po prisijungimo per OAuth", "oauth_button_text": "Mygtuko tekstas", + "oauth_client_secret_description": "Privalomas jei PKCE (Proof Key for Code Exchange) nepalaikomas pagal OAuth tiekėją", "oauth_enable_description": "Prisijungti su OAuth", "oauth_mobile_redirect_uri": "Mobiliojo peradresavimo URI", "oauth_mobile_redirect_uri_override": "Mobiliojo peradresavimo URI pakeitimas", "oauth_mobile_redirect_uri_override_description": "ÄŽjunkite, kai OAuth teikėjas nepalaiko mobiliojo URI, tokio kaip ''{callback}''", + "oauth_role_claim": "Rolės Tvirtinimas", + "oauth_role_claim_description": "Suteikti admin teises automatiÅĄkai pagal ÅĄios rolės tvirtinimo buvimą. Tvirtinimas gali turėti priskirtus arba 'vartotoją' arba 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "Tvarkyti OAuth prisijungimo nustatymus", "oauth_settings_more_details": "Detaliau apie ÅĄią funkciją galite paskaityti dokumentacijoje.", + "oauth_storage_label_claim": "Saugyklos ÅŊyma pagal tvirtinimą", + "oauth_storage_label_claim_description": "Priskirti Saugyklos ÅŊymą automatiÅĄkai pagal reikÅĄmę vartotojo tvirtinime.", + "oauth_storage_quota_claim": "Saugyklos apimties tvirtinimas", + "oauth_storage_quota_claim_description": "Priskirti vartotojo saugyklos apimties kvotą automatiÅĄkai pagal ÅĄio tvirtinimo reikÅĄmę.", "oauth_storage_quota_default": "Numatyta atminties kvota (GiB)", + "oauth_storage_quota_default_description": "Nustatoma appimties kvota GiB kai nėra nurodyta tvirtinime.", "oauth_timeout": "UÅžklausa virÅĄijo laiko limitą", "oauth_timeout_description": "Laiko limitas uÅžklausoms milisekundėmis", "password_enable_description": "Prisijungti su el. paÅĄtu ir slaptaÅžodÅžiu", "password_settings": "Prisijungimas slaptaÅžodÅžiu", "password_settings_description": "Tvarkyti prisijungimo slaptaÅžodÅžiu nustatymus", "paths_validated_successfully": "Visi keliai patvirtinti sėkmingai", + "person_cleanup_job": "IÅĄvalyti asmenis", "quota_size_gib": "Kvotos dydis (GiB)", "refreshing_all_libraries": "Perkraunamos visos bibliotekos", "registration": "Administratoriaus registracija", @@ -199,7 +223,7 @@ "reset_settings_to_default": "Atstatyti nustatymus į numatytuosius", "reset_settings_to_recent_saved": "NustatymÅŗ atstatymas į neseniai iÅĄsaugotus nustatymus", "scanning_library": "Biblioteka skenuojama", - "search_jobs": "IeÅĄkoma darbÅŗâ€Ļ", + "search_jobs": "IeÅĄkoma uÅžduočiÅŗâ€Ļ", "send_welcome_email": "SiÅŗsti sveikinimo el. laiÅĄką", "server_external_domain_settings": "IÅĄorinis domenas", "server_external_domain_settings_description": "Bendrinimo nuorodÅŗ domenas, įskaitant http(s)://", @@ -209,19 +233,36 @@ "server_settings_description": "Tvarkyti serverio nustatymus", "server_welcome_message": "Sveikinimo praneÅĄimas", "server_welcome_message_description": "ÅŊinutė, rodoma prisijungimo puslapyje.", + "sidecar_job": "Sidecar metaduomenys", + "sidecar_job_description": "Aptikti ar sinchronizuoti sidecar metaduomenis iÅĄ failÅŗ sistemos", "slideshow_duration_description": "SekundÅžiÅŗ skaičius, kiek viena nuotrauka rodoma", "smart_search_job_description": "Vykdykite maÅĄininį mokymąsi bibliotekos elementÅŗ iÅĄmaniajai paieÅĄkai", "storage_template_date_time_description": "Elemento sukÅĢrimo laiko Åžymė yra naudojama laiko informacijai", "storage_template_date_time_sample": "Pavyzdinis laikas {date}", + "storage_template_enable_description": "Aktyvuoti saugyklos ÅĄabloną", + "storage_template_hash_verification_enabled": "Aktyvuoti Hash tikrinimą", + "storage_template_hash_verification_enabled_description": "Aktyvuojamas Hash tikrinimas, neiÅĄjungti nebent gerai suprantate galimas pasekmes", + "storage_template_migration": "Saugyklos tvarkymas pagal ÅĄabloną", + "storage_template_migration_description": "Taikyti dabartinį {template} anksčiau įkeltiems duomenims", + "storage_template_migration_info": "Saugyklos tvarkyklė konvertuos visus plėtinius maÅžosiomis raidėmis. Å ablonas bus taikomas tik naujiems duomenims. Taikyti ÅĄabloną retroaktyviai anksčiau įkeltiems duomenims, paleiskite ÅĄią {job}.", + "storage_template_migration_job": "Saugyklos Tvarkymo Pagal Å abloną UÅžduotis", + "storage_template_more_details": "Daugiau detaliÅŗ apie ÅĄią funkciją, atsiÅžvelkite į Storage Template ir jo galimus implications", + "storage_template_onboarding_description_v2": "Kai aktyvuota, ÅĄi funkcija automatiÅĄkai sukurs failus pagal vartotojo-nustatytą ÅĄabloną. Daugiau informacijos, praÅĄome skaityti documentation.", + "storage_template_path_length": "Preliminarus struktÅĢros kelio ilgis/limitas:{length, number}/{limit, number}", + "storage_template_settings": "Saugyklos Å ablonas", + "storage_template_settings_description": "Tvarkyti aplankÅŗ struktÅĢrą bei failÅŗ pavadinimus įkeliamiems duomenims", + "storage_template_user_label": "{label} yra vartotojo Saugyklos ÅŊymą", "system_settings": "Sistemos nustatymai", "tag_cleanup_job": "ÅŊymÅŗ iÅĄvalymas", + "template_email_available_tags": "Savo ÅĄablone galite naudoti nurodytas kintamas reikÅĄmes:{tags}", + "template_email_if_empty": "Jei ÅĄablone tuÅĄÄia reikÅĄmė, bus naudojamas numatytas pagal nutylėjimą El. paÅĄto adresas.", "template_email_preview": "PerÅžiÅĢra", "template_email_settings": "El. paÅĄto Å ablonai", "template_settings": "PraneÅĄimÅŗ ÅĄablonai", "template_settings_description": "Tvarkyti pasirinktinius praneÅĄimÅŗ ÅĄablonus", "theme_custom_css_settings": "Individualizuotas CSS", "theme_settings": "Temos nustatymai", - "thumbnail_generation_job": "Generuoti miniatiÅĢras", + "thumbnail_generation_job": "Generuoti MiniatiÅĢras", "thumbnail_generation_job_description": "DideliÅŗ, maÅžÅŗ ir neryÅĄkiÅŗ miniatiÅĢrÅŗ generavimas kiekvienam bibliotekos elementui, taip pat miniatiÅĢrÅŗ generavimas kiekvienam asmeniui", "transcoding_acceleration_api": "Spartinimo API", "transcoding_acceleration_nvenc": "NVENC (reikalinga NVIDIA GPU)", @@ -233,16 +274,21 @@ "transcoding_audio_codec_description": "Opus yra aukÅĄÄiausios kokybės variantas, tačiau turi maÅžesnį suderinamumą su senesniais įrenginiais ar programine įranga.", "transcoding_bitrate_description": "Vaizdo įraÅĄai virÅĄija maksimalią leistiną bitÅŗ spartą arba nėra priimtino formato", "transcoding_constant_quality_mode": "Pastovios kokybės reÅžimas", + "transcoding_constant_rate_factor_description": "Video kokybės lygis. Tipinės reikÅĄmės yra 23 jei H.264, 28 jei HVEC, 31 jei VP9, ir 35 jei AV1. Kuo maÅžesnis tuo kokybiÅĄkesnis tačiau didesni failai.", "transcoding_hardware_acceleration": "Techninės įrangos spartinimas", "transcoding_hardware_decoding": "Aparatinis dekodavimas", "transcoding_max_bitrate": "Maksimalus bitÅŗ srautas", + "transcoding_max_bitrate_description": "Pasirenkant max bitrate galima pasiekti labiau nuspėjamą failÅŗ dydį su minimaliais kokybės praradimais. Prie 720p, tipinės reikÅĄmės yra 2600 kbits/s jei BP9 ar HVEC, arba 4500 kbits/s jei H.264. Neveiksnus jei pasirenkamas 0.", + "transcoding_preset_preset_description": "Kompresijos greitis. Siekiant tam tikro bitrate lėtesnis apdorojimas lems maÅžesnius failÅŗ dydÅžius ir padidins kokybę. VP9 ignoruos greičius virÅĄ \"gretesnis\" lygio.", "transcoding_target_resolution_description": "Didesnės skiriamosios gebos gali iÅĄsaugoti daugiau detaliÅŗ, tačiau jas koduoti uÅžtrunka ilgiau, failÅŗ dydÅžiai yra didesni ir gali sumaŞėti programos jautrumas.", "transcoding_video_codec": "Video kodekas", "trash_enabled_description": "ÄŽgalinti ÅĄiukÅĄliadėŞės funkcijas", "trash_number_of_days": "DienÅŗ skaičius", "trash_settings": "Å iukÅĄliadėŞės nustatymai", "trash_settings_description": "Tvarkyti ÅĄiukÅĄliadėŞės nustatymus", + "user_cleanup_job": "VartotojÅŗ iÅĄvalymas", "user_delete_delay_settings": "IÅĄtrynimo delsa", + "user_delete_delay_settings_description": "Skaičius dienÅŗ po iÅĄtrynimo kuomet vartotojo paskyrą ir susiję duomenys bus negraÅžinamai iÅĄtrinti. Vartotojo Trynimo uÅžduotis paleidÅžiama vidurnaktį ir tikrina kurie vartotojai gali bÅĢti trinami. Å io nustatymo pakeitimai bus naudojami sekančio uÅžduoties paleidimo metu.", "user_management": "NaudotojÅŗ valdymas", "user_password_has_been_reset": "Naudotojo slaptaÅžodis buvo iÅĄ naujo nustatytas:", "user_restore_description": "Naudotojo {user} paskyra bus atkurta.", @@ -305,7 +351,6 @@ "assets": "Elementai", "assets_added_count": "{count, plural, one {Pridėtas # elementas} few {Pridėti # elementai} other {Pridėta # elementÅŗ}}", "assets_added_to_album_count": "ÄŽ albumą {count, plural, one {įtrauktas # elementas} few {įtraukti # elementai} other {įtraukta # elementÅŗ}}", - "assets_added_to_name_count": "ÄŽ {hasName, select, true {{name}} other {naują}} albumą {count, plural, one {įtrauktas # elementas} few {įtraukti # elementai} other {įtraukta # elementÅŗ}}", "assets_count": "{count, plural, one {# elementas} few {# elementai} other {# elementÅŗ}}", "assets_moved_to_trash_count": "{count, plural, one {# elementas perkeltas} few {# elementai perkelti} other {# elementÅŗ perkelta}} į ÅĄiukÅĄliadėŞę", "assets_permanently_deleted_count": "{count, plural, one {# elementas iÅĄtrintas} few {# elementai iÅĄtrinti} other {# elementÅŗ iÅĄtrinta}} visam laikui", @@ -316,7 +361,11 @@ "authorized_devices": "Autorizuoti įrenginiai", "back": "Atgal", "back_close_deselect": "Atgal, uÅždaryti arba atÅžymėti", + "backup_background_service_current_upload_notification": "ÄŽkeliamas {filename}", + "backup_background_service_upload_failure_notification": "Nepavyko įkelti {filename}", "backup_controller_page_background_wifi": "Only on WiFi", + "backup_controller_page_filename": "Failo pavadinimas: {filename}[{size}]", + "backup_controller_page_uploading_file_info": "ÄŽkeliama failo info", "birthdate_saved": "Sėkmingai iÅĄsaugota gimimo data", "blurred_background": "NeryÅĄkus fonas", "bugs_and_feature_requests": "KlaidÅŗ ir funkcijÅŗ uÅžklausos", @@ -345,6 +394,7 @@ "clear_all": "IÅĄvalyti viską", "clear_message": "IÅĄvalyti praneÅĄimą", "clear_value": "IÅĄvalyti reikÅĄmę", + "client_cert_invalid_msg": "Netinkamas sertifikato failas arba neteisingas slaptaÅžodis", "close": "UÅždaryti", "collapse": "Suskleisti", "collapse_all": "Suskleisti viską", @@ -420,8 +470,11 @@ "do_not_show_again": "Daugiau nerodyti ÅĄio praneÅĄimo", "documentation": "Dokumentacija", "download": "AtsisiÅŗsti", + "download_include_embedded_motion_videos_description": "Pridėti prie judesio nuotraukÅŗ įterptus video kaip atskirą failą", "download_settings": "AtsisiÅŗsti", "downloading": "Siunčiama", + "downloading_asset_filename": "Parsisiunčiamas resursas {filename}", + "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)", "duration": "Trukmė", @@ -493,6 +546,7 @@ "unable_to_delete_import_path": "Nepavyksta iÅĄtrinti importavimo kelio", "unable_to_delete_shared_link": "Nepavyko iÅĄtrinti bendrinimo nuorodos", "unable_to_delete_user": "Nepavyksta iÅĄtrinti naudotojo", + "unable_to_download_files": "Nepavyksta atsisiÅŗsti failÅŗ", "unable_to_edit_exclusion_pattern": "Nepavyksta redaguoti iÅĄimčiÅŗ ÅĄablono", "unable_to_edit_import_path": "Nepavyksta redaguoti iÅĄimčiÅŗ kelio", "unable_to_enter_fullscreen": "Nepavyksta pereiti į viso ekrano reÅžimą", @@ -517,6 +571,7 @@ "unable_to_scan_library": "Nepavyksta nuskaityti bibliotekos", "unable_to_set_feature_photo": "Nepavyksta nustatyti mėgstamiausios nuotraukos", "unable_to_set_profile_picture": "Nepavyksta nustatyti profilio nuotraukos", + "unable_to_submit_job": "Napvyko sukurti uÅžduoties", "unable_to_trash_asset": "Nepavyko perkelti į ÅĄiukÅĄliadėŞę", "unable_to_upload_file": "Nepavyksta įkelti failo" }, @@ -539,6 +594,7 @@ "features_setting_description": "Valdyti aplikacijos funkcijas", "file_name": "Failo pavadinimas", "file_name_or_extension": "Failo pavadinimas arba plėtinys", + "filename": "Failopavadinimas", "filetype": "Failo tipas", "filter_people": "Filtruoti Åžmones", "folders": "Aplankai", @@ -574,8 +630,9 @@ }, "invite_people": "Kviesti Åžmones", "invite_to_album": "Pakviesti į albumą", + "ios_debug_info_no_sync_yet": "Jokia background sync uÅžduotis dar nebuvo paleista", "items_count": "{count, plural, one {# elementas} few {# elementai} other {# elementÅŗ}}", - "jobs": "Darbai", + "jobs": "UÅžduotys", "keep": "Palikti", "keep_all": "Palikti visus", "keyboard_shortcuts": "Spartieji klaviatÅĢros klaviÅĄai", @@ -660,6 +717,7 @@ "no_results": "Nerasta", "no_results_description": "Pabandykite sinonimą arba bendresnį raktaÅžodį", "not_in_any_album": "Nė viename albume", + "note_apply_storage_label_to_previously_uploaded assets": "Pastaba: Priskirti Saugyklos ÅŊymą prie ankčiau įkeltÅŗ iÅĄtekliu, paleiskite ÅĄÄ¯", "notes": "Pastabos", "notification_toggle_setting_description": "ÄŽjungti el. paÅĄto praneÅĄimus", "notifications": "PraneÅĄimai", @@ -707,6 +765,14 @@ "place": "Vieta", "places": "Vietos", "play_memories": "Leisti atsiminimus", + "profile": "Profilis", + "profile_drawer_app_logs": "Logai", + "profile_drawer_client_out_of_date_major": "Mobili aplikacija jau pasenusios versijos. PraÅĄome atsinaujinti į paskutinę didÅžiąją versiją.", + "profile_drawer_client_out_of_date_minor": "Mobili aplikacija jau pasenusios versijos. PraÅĄome atsinaujinti į paskutinę maŞąją versiją.", + "profile_drawer_client_server_up_to_date": "Klientas ir Serveris yra atnaujinti", + "profile_drawer_github": "GitHub", + "profile_drawer_server_out_of_date_major": "Serveris jau yra pasenusios versijos. PraÅĄome atsinaujinti į paskutinę didÅžiąją versiją.", + "profile_drawer_server_out_of_date_minor": "Serveris jau yra pasenusios versijos. PraÅĄome atsinaujinti į paskutinę maŞąją versiją.", "profile_image_of_user": "{user} profilio nuotrauka", "profile_picture_set": "Profilio nuotrauka nustatyta.", "public_album": "VieÅĄas albumas", @@ -893,7 +959,7 @@ "show_albums": "Rodyti albumus", "show_all_people": "Rodyti visus asmenis", "show_and_hide_people": "Rodyti ir paslėpti Åžmones", - "show_file_location": "Rodyti rinkmenos vietą", + "show_file_location": "Rodyti failo vietą", "show_gallery": "Rodyti galeriją", "show_hidden_people": "Rodyti paslėptus asmenis", "show_in_timeline": "Rodyti laiko skalėje", @@ -936,6 +1002,7 @@ "status": "Statusas", "stop_casting": "Nutraukti transliavimą", "storage": "Saugykla", + "storage_label": "Saugyklos ÅŊyma", "storage_usage": "Naudojama {used} iÅĄ {available}", "submit": "Pateikti", "suggestions": "PasiÅĢlymai", diff --git a/i18n/lv.json b/i18n/lv.json index 3fcf228612..2e6b702e22 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -88,11 +88,14 @@ "machine_learning_url_description": "MaÅĄÄĢnmācÄĢÅĄanās servera URL", "manage_concurrency": "VienlaicÄĢgas darbÄĢbas pārvaldÄĢba", "manage_log_settings": "ÅŊurnāla iestatÄĢjumu pārvaldÄĢba", + "map_dark_style": "TumÅĄais stils", "map_gps_settings": "Kartes un GPS iestatÄĢjumi", "map_gps_settings_description": "KarÅĄu un GPS (apgrieztās ÄŖeokodÄ“ÅĄanas) iestatÄĢjumu pārvaldÄĢba", + "map_light_style": "GaiÅĄais stils", "map_manage_reverse_geocoding_settings": "Reversās ÄŖeokodÄ“ÅĄanas iestatÄĢjumu pārvaldÄĢba", "map_settings": "Karte", "map_settings_description": "Kartes iestatÄĢjumu pārvaldÄĢba", + "map_style_description": "URL uz style.json kartes tēmu", "metadata_extraction_job": "Metadatu iegÅĢÅĄana", "metadata_settings": "Metadatu iestatÄĢjumi", "metadata_settings_description": "Metadatu iestatÄĢjumu pārvaldÄĢba", @@ -110,6 +113,10 @@ "notification_email_test_email_sent": "Uz {email} ir nosÅĢtÄĢts testa e-pasts. LÅĢdzu, pārbaudi savu iesÅĢtni.", "notification_settings": "Paziņojumu iestatÄĢjumi", "notification_settings_description": "Paziņojumu iestatÄĢjumu, tostarp e-pasta, pārvaldÄĢba", + "oauth_auto_launch": "Palaist automātiski", + "oauth_auto_launch_description": "Pie navigācijas uz pieslēgÅĄanās lapu automātiski uzsākt OAuth pieslēgÅĄanās plÅĢsmu", + "oauth_auto_register": "Automātiska reÄŖistrācija", + "oauth_auto_register_description": "Pēc pieslēgÅĄanās ar OAuth automātiski reÄŖistrēt jaunus lietotājus", "oauth_button_text": "Pogas teksts", "oauth_enable_description": "Pieslēgties ar OAuth", "oauth_settings": "OAuth", @@ -124,6 +131,7 @@ "require_password_change_on_login": "PieprasÄĢt lietotājam mainÄĢt paroli pēc pirmās pieteikÅĄanās", "scanning_library": "Skenē bibliotēku", "search_jobs": "Meklēt uzdevumusâ€Ļ", + "server_public_users": "Publiski lietotāji", "server_settings": "Servera iestatÄĢjumi", "server_settings_description": "Servera iestatÄĢjumu pārvaldÄĢba", "server_welcome_message": "Sveiciena ziņa", @@ -134,7 +142,12 @@ "storage_template_path_length": "Aptuvenais ceÄŧa garuma ierobeÅžojums: {length, number}/{limit, number}", "storage_template_settings": "Krātuves veidne", "system_settings": "Sistēmas iestatÄĢjumi", + "template_email_available_tags": "Sagatavē var izmantot ÅĄos mainÄĢgos: {tags}", + "template_email_if_empty": "Ja sagatave ir tukÅĄa, tiks izmantots noklusējuma e-pasts.", + "template_email_invite_album": "Albuma ielÅĢguma sagatave", "template_email_preview": "PriekÅĄskatÄĢjums", + "template_email_settings": "E-pasta sagataves", + "template_email_update_album": "Atjaunināt albuma sagatavi", "template_settings_description": "Pielāgotu paziņojumu veidņu pārvaldÄĢba", "theme_custom_css_settings": "Pielāgots CSS", "theme_custom_css_settings_description": "Cascading Style Sheets Äŧauj pielāgot Immich izskatu.", @@ -219,7 +232,9 @@ "are_these_the_same_person": "Vai ÅĄÄĢ ir tā pati persona?", "asset_action_delete_err_read_only": "Nevar dzēst read only aktÄĢvu(-s), notiek izlaiÅĄana", "asset_action_share_err_offline": "Nevar iegÅĢt bezsaistes aktÄĢvu(-s), notiek izlaiÅĄana", + "asset_added_to_album": "Pievienots albumam", "asset_adding_to_album": "Pievieno albumamâ€Ļ", + "asset_description_updated": "Faila apraksts ir atjaunināts", "asset_list_group_by_sub_title": "Grupēt pēc", "asset_list_layout_settings_dynamic_layout_title": "Dinamiskais izkārtojums", "asset_list_layout_settings_group_automatically": "Automātiski", @@ -228,6 +243,7 @@ "asset_list_layout_sub_title": "Izvietojums", "asset_list_settings_subtitle": "FotoreÅžÄŖa izkārtojuma iestatÄĢjumi", "asset_list_settings_title": "FotoreÅžÄŖis", + "asset_uploaded": "AugÅĄupielādēts", "asset_uploading": "AugÅĄupielādēâ€Ļ", "asset_viewer_settings_title": "AktÄĢvu SkatÄĢtājs", "assets": "aktÄĢvi", @@ -339,6 +355,7 @@ "clear": "NotÄĢrÄĢt", "clear_all": "NotÄĢrÄĢt visu", "clear_value": "NotÄĢrÄĢt vērtÄĢbu", + "client_cert_subtitle": "Atbalsta tikai PKCS12 (.p12, .pfx) formātu. Sertifikātu importÄ“ÅĄana/noņemÅĄana ir pieejama tikai pirms pieslēgÅĄanās", "client_cert_title": "SSL klienta sertifikāts", "clockwise": "PulksteņrādÄĢtāja virzienā", "close": "Aizvērt", @@ -468,6 +485,7 @@ "cant_get_faces": "Nevar iegÅĢt sejas", "cant_search_people": "Neizdevās veikt peronu meklÄ“ÅĄanu", "failed_to_create_album": "Neizdevās izveidot albumu", + "profile_picture_transparent_pixels": "Profila attēlos nevar bÅĢt caurspÄĢdÄĢgi pikseÄŧi. LÅĢdzu, palielini un/vai pārvieto attēlu.", "unable_to_change_description": "Neizdevās nomainÄĢt aprakstu", "unable_to_create_user": "Neizdevās izveidot lietotāju", "unable_to_delete_user": "Neizdevās dzēst lietotāju", @@ -504,7 +522,11 @@ "features_setting_description": "Lietotnes funkciju pārvaldÄĢba", "filename": "Faila nosaukums", "filetype": "Faila tips", + "folder": "Mape", + "folder_not_found": "Mape nav atrasta", "folders": "Mapes", + "gcast_enabled": "Google Cast", + "get_help": "Saņemt palÄĢdzÄĢbu", "haptic_feedback_switch": "IestatÄĢt haptisku reakciju", "haptic_feedback_title": "Haptiska Reakcija", "has_quota": "Ir kvota", @@ -529,10 +551,12 @@ "hour": "Stunda", "id": "ID", "image": "Attēls", + "image_saved_successfully": "Attēls saglabāts", "image_viewer_page_state_provider_download_started": "Lejupielāde Uzsākta", "image_viewer_page_state_provider_download_success": "Lejupielāde izdevās", "image_viewer_page_state_provider_share_error": "KopÄĢgoÅĄanas KÄŧÅĢda", "immich_logo": "Immich logo", + "immich_web_interface": "Immich tÄĢmekÄŧa saskarne", "import_from_json": "Importēt no JSON", "import_path": "Importa ceÄŧÅĄ", "in_albums": "{count, plural, one {# albumā} other {# albumos}}", @@ -545,18 +569,25 @@ "night_at_midnight": "Katru dienu pusnaktÄĢ", "night_at_twoam": "Katru dienu 2.00 naktÄĢ" }, + "invalid_date": "NederÄĢgs datums", + "invalid_date_format": "NederÄĢgs datuma formāts", "invite_people": "IelÅĢgt cilvēkus", "invite_to_album": "Uzaicināt albumā", + "ios_debug_info_last_sync_at": "Pēdējā sinhronizācija {dateTime}", + "ios_debug_info_no_processes_queued": "Nav ierindotu fona procesu", "jobs": "Uzdevumi", "keep": "Paturēt", "keep_all": "Paturēt visus", + "keep_this_delete_others": "Paturēt ÅĄo, dzēst citus", "keyboard_shortcuts": "TastatÅĢras saÄĢsnes", "language": "Valoda", + "language_search_hint": "Meklēt valodas...", "language_setting_description": "Izvēlieties vēlamo valodu", "last_seen": "Pēdējo reizi redzēts", "latest_version": "Jaunākā versija", "latitude": "Äĸeogrāfiskais platums", "leave": "Paturēt", + "lens_model": "ObjektÄĢva modelis", "let_others_respond": "Äģaut citiem atbildēt", "level": "LÄĢmenis", "library": "Bibliotēka", @@ -600,7 +631,7 @@ "longitude": "Äĸeogrāfiskais garums", "look": "Izskats", "loop_videos_description": "Iespējot, lai automātiski videoklips tiktu cikliski palaists detaÄŧu skatÄĢtājā.", - "make": "Firma", + "make": "RaÅžotājs", "manage_shared_links": "KopÄĢgoto saiÅĄu pārvaldÄĢba", "manage_sharing_with_partners": "KoplietoÅĄanas ar partneriem pārvaldÄĢba", "manage_the_app_settings": "Lietotnes iestatÄĢjumu pārvaldÄĢba", @@ -676,6 +707,9 @@ "next_memory": "Nākamā atmiņa", "no": "Nē", "no_albums_message": "Izveido albumu, lai organizētu savas fotogrāfijas un video", + "no_albums_with_name_yet": "Izskatās, ka tev vēl nav albumu ar ÅĄÄdu nosaukumu.", + "no_albums_yet": "Izskatās, ka tev vēl nav neviena albuma.", + "no_archived_assets_message": "Arhivē fotoattēlus un videoklipus, lai paslēptu tos no Fotoattēli skata", "no_assets_message": "NOKLIKÅ ÄļINIET, LAI AUGÅ UPIELĀDĒTU SAVU PIRMO FOTOATTĒLU", "no_assets_to_show": "Nav uzrādāmo aktÄĢvu", "no_duplicates_found": "Dublikāti netika atrasti.", @@ -699,6 +733,10 @@ "official_immich_resources": "Oficiālie Immich resursi", "offline": "Bezsaistē", "ok": "Labi", + "onboarding": "UzņemÅĄana", + "onboarding_locale_description": "Izvēlies vēlamo valodu. To vēlāk var mainÄĢt iestatÄĢjumos.", + "onboarding_theme_description": "Izvēlies savas instances krāsu motÄĢvu. To vēlāk var mainÄĢt iestatÄĢjumos.", + "onboarding_user_welcome_description": "Sāksim darbu!", "online": "TieÅĄsaistē", "only_favorites": "Tikai izlase", "open_in_map_view": "Atvērt kartes skatā", @@ -706,13 +744,16 @@ "open_the_search_filters": "Atvērt meklÄ“ÅĄanas filtrus", "options": "IestatÄĢjumi", "or": "vai", + "organize_your_library": "Bibliotēkas organizÄ“ÅĄana", "original": "oriÄŖināls", "other": "Citi", "other_devices": "Citas ierÄĢces", "other_variables": "Citi mainÄĢgie", "owned": "ÄĒpaÅĄumā", "owner": "ÄĒpaÅĄnieks", + "partner": "Partneris", "partner_can_access": "{partner} var piekÄŧÅĢt", + "partner_can_access_location": "Fotogrāfiju uzņemÅĄanas vieta", "partner_list_user_photos": "{user} fotoattēli", "partner_list_view_all": "ApskatÄĢt visu", "partner_page_empty_message": "JÅĢsu fotogrāfijas pagaidām nav kopÄĢgotas ar nevienu partneri.", @@ -726,6 +767,7 @@ "password_does_not_match": "Parole nesakrÄĢt", "path": "CeÄŧÅĄ", "pause": "Pauzēt", + "pause_memories": "Pauzēt atmiņas", "paused": "Nopauzēts", "people": "Cilvēki", "permission_onboarding_back": "AtpakaÄŧ", @@ -738,48 +780,78 @@ "permission_onboarding_request": "Immich nepiecieÅĄama atÄŧauja skatÄĢt jÅĢsu fotoattēlus un videoklipus.", "person": "Persona", "photos": "Fotoattēli", + "photos_and_videos": "Fotogrāfijas un video", "photos_from_previous_years": "Fotogrāfijas no iepriekÅĄÄ“jiem gadiem", + "pick_a_location": "Izvēlies atraÅĄanās vietu", "pin_verification": "PIN koda pārbaude", "place": "AtraÅĄanās vieta", "places": "Vietas", + "play": "Atskaņot", + "play_memories": "Atskaņot atmiņas", "please_auth_to_access": "Lai piekÄŧÅĢtu, lÅĢdzu, autentificējieties", "port": "Ports", "preferences_settings_title": "IestatÄĢjumi", "preview": "PriekÅĄskatÄĢjums", "previous": "IepriekÅĄÄ“jais", + "previous_memory": "IepriekÅĄÄ“jā atmiņa", "privacy": "Privātums", "profile": "Profils", "profile_drawer_app_logs": "ÅŊurnāli", "profile_drawer_client_out_of_date_major": "Mobilā lietotne ir novecojusi. LÅĢdzu, atjaunini to uz jaunāko pamatversiju.", "profile_drawer_client_out_of_date_minor": "Mobilā lietotne ir novecojusi. LÅĢdzu, atjaunini to uz jaunāko papildversiju.", "profile_drawer_client_server_up_to_date": "Klients un serveris ir atjaunināti", + "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Serveris ir novecojis. LÅĢdzu, atjaunini to uz jaunāko pamatversiju.", "profile_drawer_server_out_of_date_minor": "Serveris ir novecojis. LÅĢdzu, atjaunini to uz jaunāko papildversiju.", + "profile_image_of_user": "{user} profila attēls", + "profile_picture_set": "Profila attēls iestatÄĢts.", + "public_album": "Publisks albums", "purchase_account_info": "AtbalstÄĢtājs", + "purchase_activated_subtitle": "Paldies, ka atbalstāt Immich un atvērtā koda programmatÅĢru", + "purchase_activated_time": "Aktivizēts {date}", + "purchase_activated_title": "Tava atslēga ir sekmÄĢgi aktivizēta", + "purchase_button_activate": "Aktivizēt", "purchase_button_buy": "Pirkt", + "purchase_button_buy_immich": "Iegādāties Immich", "purchase_button_never_show_again": "Nekad vairs nerādÄĢt", "purchase_button_reminder": "Atgādināt man pēc 30 dienām", "purchase_button_remove_key": "Noņemt atslēgu", "purchase_button_select": "Izvēlēties", + "purchase_failed_activation": "Neizdevās aktivizēt! LÅĢdzu, pārbaudi savu e-pastu, lai iegÅĢtu pareizo produkta atslēgu!", "purchase_individual_description_2": "AtbalstÄĢtāja statuss", "purchase_input_suggestion": "Vai tev ir produkta atslēga? Ievadi atslēgu zemāk", "purchase_license_subtitle": "Nopērc Immich licenci, lai atbalstÄĢtu turpmāku pakalpojuma attÄĢstÄĢbu", "purchase_lifetime_description": "Pirkums uz mÅĢÅžu", "purchase_option_title": "IEGĀDES IESPĒJAS", + "purchase_panel_info_1": "Immich veidoÅĄanai ir nepiecieÅĄams daudz laika un pÅĢÄŧu, un pie tā strādā pilna laika inÅženieri, lai padarÄĢtu to pēc iespējas labāku. MÅĢsu misija ir panākt, lai atvērtā koda programmatÅĢra un ētiska uzņēmējdarbÄĢbas prakse kÄŧÅĢtu par ilgtspējÄĢgu ienākumu avotu izstrādātājiem un izveidotu privātumu respektējoÅĄu ekosistēmu ar reālām alternatÄĢvām ekspluatējoÅĄiem mākoņpakalpojumiem.", + "purchase_panel_info_2": "Tā kā mēs esam apņēmuÅĄies nepievienot maksas funkcionalitāti, ÅĄis pirkums nepieÅĄÄˇirs jums nekādas papildu Immich funkcijas. Mēs paÄŧaujamies uz tādiem lietotājiem kā jÅĢs, lai atbalstÄĢtu nepārtrauktu Immich attÄĢstÄĢbu.", "purchase_panel_title": "Atbalsti projektu", "purchase_remove_product_key": "Noņemt produkta atslēgu", + "purchase_remove_product_key_prompt": "Vai tieÅĄÄm vēlaties noņemt produkta atslēgu?", "purchase_remove_server_product_key": "Noņemt servera produkta atslēgu", + "purchase_remove_server_product_key_prompt": "Vai tieÅĄÄm vēlaties noņemt Servera produkta atslēgu?", "purchase_server_description_1": "Visam serverim", "purchase_server_description_2": "AtbalstÄĢtāja statuss", "purchase_server_title": "Serveris", "purchase_settings_server_activated": "Servera produkta atslēgu pārvalda administrators", "rating_clear": "Noņemt vērtējumu", + "rating_description": "RādÄĢt EXIF vērtējumu informācijas panelÄĢ", + "reaction_options": "Reakcijas iespējas", "read_changelog": "LasÄĢt izmaiņu sarakstu", "recently_added_page_title": "Nesen Pievienotais", + "refresh_thumbnails": "Atsvaidzināt sÄĢktēlus", + "refreshed": "Atsvaidzināts", "remove": "Noņemt", + "remove_assets_title": "Izņemt failus?", + "remove_deleted_assets": "Izņemt dzēstos failus", "remove_from_album": "Noņemt no albuma", + "remove_from_album_action_prompt": "No albuma izņemti {count} faili", "remove_from_favorites": "Noņemt no izlases", + "remove_from_lock_folder_action_prompt": "No slēgtās mapes izņemti {count} faili", "remove_from_locked_folder": "Izņemt no slēgtās mapes", + "remove_memory": "Noņemt atmiņu", + "remove_photo_from_memory": "Noņemt fotogrāfiju no ÅĄÄĢs atmiņas", + "remove_url": "Noņemt URL", "remove_user": "Noņemt lietotāju", "removed_api_key": "Noņēma API atslēgu: {name}", "removed_from_archive": "Noņēma no arhÄĢva", @@ -790,6 +862,11 @@ "replace_with_upload": "Aizstāt ar augÅĄupielādi", "require_user_to_change_password_on_first_login": "PieprasÄĢt lietotājam mainÄĢt paroli pēc pirmās pieteikÅĄanās", "rescan": "Pārskenēt atkārtoti", + "reset": "AtiestatÄĢt", + "reset_password": "AtiestatÄĢt paroli", + "reset_people_visibility": "AtiestatÄĢt cilvēku redzamÄĢbu", + "reset_pin_code": "AtiestatÄĢt PIN kodu", + "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", "restore": "Atjaunot", @@ -802,6 +879,7 @@ "role_editor": "Redaktors", "role_viewer": "SkatÄĢtājs", "save": "Saglabāt", + "save_to_gallery": "Saglabāt galerijā", "saved_api_key": "API atslēga saglabāta", "saved_profile": "Profils saglabāts", "saved_settings": "IestatÄĢjumi saglabāti", @@ -813,9 +891,23 @@ "scanning_for_album": "Skenē albumu...", "search": "Meklēt", "search_albums": "Meklēt albumus", + "search_by_context": "Meklēt pēc konteksta", + "search_by_description": "Meklēt pēc apraksta", + "search_by_description_example": "Pārgājiens LÄĢgatnē", + "search_by_filename": "Meklēt pēc faila nosaukuma vai paplaÅĄinājuma", "search_by_filename_example": "piemēram, IMG_1234.JPG vai PNG", + "search_camera_make": "Meklēt pēc fotokameras raÅžotāja...", + "search_camera_model": "Meklēt pēc fotokameras modeÄŧa...", + "search_city": "Meklēt pēc pilsētas...", + "search_country": "Meklēt pēc valsts...", "search_filter_apply": "Lietot filtru", + "search_filter_camera_title": "Izvēlies fotokameras veidu", + "search_filter_date": "Datums", "search_filter_display_option_not_in_album": "Nav albumā", + "search_filter_filename": "Meklēt pēc faila nosaukuma", + "search_filter_location": "AtraÅĄanās vieta", + "search_filter_location_title": "Izvēlies atraÅĄanās vietu", + "search_for_existing_person": "Meklēt esoÅĄu personu", "search_no_people": "Nav cilvēku", "search_no_people_named": "Nav cilvēku ar vārdu \"{name}\"", "search_page_categories": "Kategorijas", @@ -836,6 +928,7 @@ "second": "Sekunde", "select_album_cover": "Izvēlieties albuma vāciņu", "select_all_duplicates": "AtlasÄĢt visus dublikātus", + "select_from_computer": "Izvēlēties no datora", "select_photos": "Fotoattēlu Izvēle", "select_user_for_sharing_page_err_album": "Neizdevās izveidot albumu", "server_info_box_app_version": "Aplikācijas Versija", @@ -1031,6 +1124,7 @@ "viewer_stack_use_as_main_asset": "Izmantot kā Galveno AktÄĢvu", "viewer_unstack": "At-Stekot", "waiting": "Gaida", + "warning": "BrÄĢdinājums", "week": "NedēÄŧa", "welcome": "Laipni lÅĢgti", "welcome_to_immich": "Laipni lÅĢgti Immich", diff --git a/i18n/mn.json b/i18n/mn.json index 8a18a1d5e5..85092fef0d 100644 --- a/i18n/mn.json +++ b/i18n/mn.json @@ -15,10 +15,13 @@ "add_a_name": "ĐŅŅ€ ĶŠĐŗĶŠŅ…", "add_a_title": "Đ“Đ°Ņ€Ņ‡Đ¸Đŗ ĐžŅ€ŅƒŅƒĐģĐ°Ņ…", "add_endpoint": "Endpoint ĐŊŅĐŧŅŅ…", + "add_import_path": "ИĐŧĐŋĐžŅ€Ņ‚ĐģĐžŅ… СаĐŧ ĐŊŅĐŧŅŅ…", "add_location": "Đ‘Đ°ĐšŅ€ŅˆĐ¸Đģ ĐžŅ€ŅƒŅƒĐģĐ°Ņ…", "add_more_users": "Ķ¨ĶŠŅ€ Ņ…ŅŅ€ŅĐŗĐģŅĐŗŅ‡Đ¸Đ´ ĐŊŅĐŧŅŅ…", "add_partner": "ĐĨаĐŧŅ‚Ņ€Đ°ĐŗŅ‡ ĐŊŅĐŧŅŅ…", + "add_path": "ЗаĐŧ ĐŊŅĐŧŅŅ…", "add_photos": "Đ—ŅƒŅ€Đ°Đŗ ĐŊŅĐŧŅŅ…", + "add_tag": "Đ¨ĐžŅˆĐŗĐž ĐŊŅĐŧŅŅ…", "add_to_album": "ĐĻĐžĐŧĐžĐŗŅ‚ ĐžŅ€ŅƒŅƒĐģĐ°Ņ…", "add_to_album_bottom_sheet_added": "{album}-Đ´ ĐŊŅĐŧĐģŅŅ", "add_to_album_bottom_sheet_already_exists": "{album}-Đ´ аĐģҌ Ņ…ŅĐ´Đ¸ĐšĐŊ ĐžŅ€ŅĐžĐŊ йаКĐŊа", @@ -28,6 +31,7 @@ "added_to_favorites": "Đ”ŅƒŅ€Ņ‚Đ°Đš ĐˇŅƒŅ€ĐŗĐ°ĐŊĐ´ ĐŊŅĐŧŅŅ…", "added_to_favorites_count": "Đ”ŅƒŅ€Ņ‚Đ°Đš ĐˇŅƒŅ€Đ°ĐŗĐŊŅƒŅƒĐ´Đ°Đ´ {count, number} ĐŊŅĐŧŅĐŗĐ´ĐģŅŅ", "admin": { + "admin_user": "АдĐŧиĐŊ Ņ…ŅŅ€ŅĐŗĐģŅĐŗŅ‡", "authentication_settings": "ĐĸаĐŊиĐŊ ĐŊŅĐ˛Ņ‚Ņ€ŅĐģŅ‚ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐž", "authentication_settings_description": "ĐŅƒŅƒŅ† Ō¯ĐŗĐ¸ĐšĐŊ ŅƒĐ´Đ¸Ņ€Đ´ĐģĐ°ĐŗĐ°, OAuth йОĐģĐžĐŊ ĐąŅƒŅĐ°Đ´ Ņ‚Đ°ĐŊиĐŊ ĐŊŅĐ˛Ņ‚Ņ€ŅĐģŅ‚Đ¸ĐšĐŊ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐž", "authentication_settings_disable_all": "Đ‘Ō¯Ņ… ĐŊŅĐ˛Ņ‚Ņ€ŅŅ… Đ°Ņ€ĐŗŅƒŅƒĐ´Ņ‹Đŗ Đ¸Đ´ŅĐ˛Ņ…Đ¸ĐŗŌ¯Đš йОĐģĐŗĐžŅ…Đ´ĐžĐž Đ¸Ņ‚ĐŗŅĐģŅ‚ŅĐš йаКĐŊа ҃҃? ĐŅĐ˛Ņ‚Ņ€ŅŅ… Ō¯ĐšĐģĐ´ŅĐģ ĐąŌ¯Ņ€ŅĐŊ Đ¸Đ´ŅĐ˛Ņ…Đ¸ĐŗŌ¯Đš йОĐģĐŊĐž.", @@ -35,11 +39,15 @@ "backup_database": "Ķ¨ĐŗĶŠĐŗĐ´ĐģиКĐŊ ŅĐ°ĐŊĐŗĐ¸ĐšĐŊ даĐŧĐŋ Ō¯Ō¯ŅĐŗŅŅ…", "backup_database_enable_description": "Ķ¨ĐŗĶŠĐŗĐ´ĐģиКĐŊ ŅĐ°ĐŊĐŗĐ¸ĐšĐŊ даĐŧĐŋ Đ¸Đ´ŅĐ˛Ņ…Đ¸ĐļŌ¯Ō¯ĐģŅŅ…", "backup_keep_last_amount": "͍ĐŧĐŊĶŠŅ… Ņ…ŅĐ´ŅĐŊ даĐŧĐŋŅ‹Đŗ Ņ…Đ°Đ´ĐŗĐ°ĐģĐ°Ņ… Đ˛Ņ", + "backup_settings": "Đ”Đ°Ņ‚Đ°ĐąĐ°Đˇ даĐŧĐŋ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐž", + "backup_settings_description": "Đ”Đ°Ņ‚Đ°ĐąĐ°ĐˇĐ°Đ°Ņ даĐŧĐŋ Ņ…Đ¸ĐšŅ… Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐžĐŊŅƒŅƒĐ´.", "config_set_by_file": "ĐĸĐžŅ…Đ¸Ņ€ĐŗĐžĐžĐŗ ĐžĐ´ĐžĐžĐŗĐžĐžŅ€ Ņ„Đ°ĐšĐģĐ°Đ°Ņ Đ°Đ˛Ņ‡ йаКĐŊа", "confirm_delete_library": "Đĸа {library} ĐŗŅŅŅĐŊ ŅĐ°ĐŊĐŗ ŅƒŅŅ‚ĐŗĐ°Ņ…Đ´Đ°Đ° Đ¸Ņ‚ĐŗŅĐģŅ‚ŅĐš йаКĐŊа ҃҃?", "confirm_delete_library_assets": "Đĸа ŅĐŊŅ ŅĐ°ĐŊĐŗ ŅƒŅŅ‚ĐŗĐ°Ņ…Đ´Đ°Đ° Đ¸Ņ‚ĐŗŅĐģŅ‚ŅĐš йаКĐŊа ҃҃? Đ­ĐŊŅ Ō¯ĐšĐģĐ´ĐģŅŅŅ€ Ņ‚Đ°ĐŊŅ‹ {count, plural, one {# contained asset} other {all # contained assets}} ҁĐĩŅ€Đ˛ĐĩŅ€ŅŅŅ ŅƒŅŅ‚Đ°Ņ… ĐąĶŠĐŗĶŠĶŠĐ´ ĐąŅƒŅ†Đ°Đ°Ņ… йОĐģĐžĐŧĐļĐŗŌ¯Đš. Đ“ŅŅ…Đ´ŅŅ Ņ„Đ°ĐšĐģŅƒŅƒĐ´ Đ´Đ¸ŅĐē Đ´ŅŅŅ€ŅŅ Ō¯ĐģĐ´ŅĐŊŅ.", "confirm_email_below": "Đ‘Đ°Ņ‚Đ°ĐģĐŗĐ°Đ°Đļ҃҃ĐģĐ°Ņ…Ņ‹ĐŊ Ņ‚ŅƒĐģĐ´ Ņ‚Đ° \"{email}\" ĐŗŅĐļ ĐąĐ¸Ņ‡ĐŊŅ Ō¯Ō¯", "confirm_reprocess_all_faces": "Đ‘Ō¯Ņ… Ņ†Đ°Ņ€Đ°ĐšĐŗ Đ´Đ°Ņ…Đ¸ĐŊ ĐŋŅ€ĐžŅ†Đĩҁҁ Ņ…Đ¸ĐšŅ… Ō¯Ō¯? ĐĸŅĐŗĐ˛ŅĐģ ĐąŌ¯Ņ… ĐŊŅŅ€Ņ Đ°Ņ€Đ¸ĐģĐ°Ņ… йОĐģĐŊĐž.", + "confirm_user_password_reset": "{user}-иКĐŊ ĐŊŅƒŅƒŅ† Ō¯ĐŗĐ¸ĐšĐŗ Đ´Đ°Ņ…Đ¸ĐŊ Ņ‚ĐžŅ…Đ¸Ņ€ŅƒŅƒĐģĐ°Ņ… ҃҃?", + "confirm_user_pin_code_reset": "{user} Ņ…ŅŅ€ŅĐŗĐģŅĐŗŅ‡Đ¸ĐšĐŊ PIN code Đ´Đ°Ņ…Đ¸ĐŊ Ņ‚ĐžŅ…Đ¸Ņ€ŅƒŅƒĐģĐ°Ņ… ҃҃?", "face_detection": "ĐŌ¯Ō¯Ņ€ иĐģŅ€Ō¯Ō¯ĐģŅŅ…", "image_quality": "ЧаĐŊĐ°Ņ€", "job_settings": "АĐļĐģŅ‹ĐŊ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐž", diff --git a/i18n/mr.json b/i18n/mr.json index 6c2ab7406c..781174c79e 100644 --- a/i18n/mr.json +++ b/i18n/mr.json @@ -4,6 +4,7 @@ "account_settings": "ā¤–ā¤žā¤¤āĨ‡ ā¤ĩāĨā¤¯ā¤ĩ⤏āĨā¤Ĩā¤ž", "acknowledge": "ā¤Žā¤žā¤¨āĨā¤¯ā¤¤ā¤ž", "action": "⤕āĨƒā¤¤āĨ€", + "action_common_update": "⤅ā¤ĻāĨā¤¯ā¤¯ā¤žā¤ĩ⤤", "actions": "⤕āĨƒā¤¤āĨā¤¯āĨ‡", "active": "⤏⤕āĨā¤°ā¤ŋ⤝", "activity": "⤗⤤ā¤ŋā¤ĩā¤ŋ⤧ā¤ŋ", @@ -13,6 +14,7 @@ "add_a_location": "ā¤ā¤• ⤏āĨā¤Ĩ⤺ ā¤Ÿā¤žā¤•ā¤ž", "add_a_name": "ā¤¨ā¤žā¤ĩ ā¤Ÿā¤žā¤•ā¤ž", "add_a_title": "ā¤ļāĨ€ā¤°āĨā¤ˇā¤• ā¤Ÿā¤žā¤•ā¤ž", + "add_endpoint": "ā¤ā¤‚ā¤Ąā¤ĒāĨ‰ā¤‡ā¤‚ā¤Ÿ ⤜āĨ‹ā¤Ąā¤ž", "add_exclusion_pattern": "⤅ā¤Ēā¤ĩā¤žā¤Ļ ā¤¨ā¤ŽāĨā¤¨ā¤ž ⤜āĨ‹ā¤Ąā¤ž", "add_import_path": "ā¤†ā¤¯ā¤žā¤¤ ā¤Žā¤žā¤°āĨā¤— ā¤Ÿā¤žā¤•ā¤ž", "add_location": "⤏āĨā¤Ĩ⤺ ā¤Ÿā¤žā¤•ā¤ž", @@ -20,8 +22,11 @@ "add_partner": "ā¤­ā¤žā¤—āĨ€ā¤Ļā¤žā¤° ⤜āĨ‹ā¤Ąā¤ž", "add_path": "ā¤Žā¤žā¤°āĨā¤— ā¤Ÿā¤žā¤•ā¤ž", "add_photos": "ā¤›ā¤žā¤¯ā¤žā¤šā¤ŋ⤤āĨā¤°āĨ‡ ⤜āĨ‹ā¤Ąā¤ž", + "add_tag": "⤟āĨ…⤗ ⤜āĨ‹ā¤Ąā¤ž", "add_to": "⤤āĨā¤¯ā¤ž ā¤Žā¤§āĨā¤¯āĨ‡ ⤜āĨ‹ā¤Ąā¤žâ€Ļ", "add_to_album": "⤏⤂⤗āĨā¤°ā¤šā¤žā¤¤ ā¤Ÿā¤žā¤•ā¤ž", + "add_to_album_bottom_sheet_added": "{album} ā¤Žā¤§āĨā¤¯āĨ‡ ⤜āĨ‹ā¤Ąā¤˛āĨ‡ ⤗āĨ‡ā¤˛āĨ‡", + "add_to_album_bottom_sheet_already_exists": "⤆⤧āĨ€ā¤š {album} ā¤Žā¤§āĨā¤¯āĨ‡ ā¤†ā¤šāĨ‡", "add_to_shared_album": "ā¤¸ā¤žā¤Žā¤žā¤¯ā¤ŋ⤕ ⤏⤂⤗āĨā¤°ā¤šā¤žā¤¤ ā¤Ÿā¤žā¤•ā¤ž", "add_url": "URL ⤜āĨ‹ā¤Ąā¤ž", "added_to_archive": "⤏⤂⤗āĨā¤°ā¤šā¤žā¤˛ā¤¯ā¤žā¤¤ ⤜āĨ‹ā¤Ąā¤˛āĨ‡", @@ -29,6 +34,7 @@ "added_to_favorites_count": "⤆ā¤ĩā¤Ąā¤¤āĨā¤¯ā¤žā¤¤ {count, number} ā¤Ÿā¤žā¤•ā¤˛āĨ‡", "admin": { "add_exclusion_pattern_description": "⤅ā¤Ēā¤ĩā¤žā¤Ļ ⤅⤍āĨā¤•āĨ‚⤞⤍ ⤜āĨ‹ā¤Ąā¤ž. ** ⤆⤪ā¤ŋ ? ā¤¯ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤—ā¤žā¤¤ ⤗āĨā¤˛āĨ‹ā¤Ŧā¤ŋ⤂⤗ ā¤¸ā¤Žā¤°āĨā¤Ĩā¤ŋ⤤ ā¤†ā¤šāĨ‡. ⤕āĨ‹ā¤Ŗā¤¤āĨā¤¯ā¤žā¤šāĨ€ \"Raw\" ā¤¨ā¤žā¤ĩā¤žā¤šāĨā¤¯ā¤ž ⤍ā¤ŋ⤰āĨā¤ĻāĨ‡ā¤ļā¤ŋ⤕āĨ‡ā¤Žā¤§āĨ€ā¤˛ ⤏⤰āĨā¤ĩ ā¤–ā¤¤ā¤žā¤ĩ⤪āĨā¤¯ā¤ž ā¤ĻāĨā¤°āĨā¤˛ā¤•āĨā¤ˇāĨ€ā¤¤ ⤕⤰⤪āĨā¤¯ā¤žā¤¸ā¤žā¤ āĨ€ \"/Raw/\" ā¤ĩā¤žā¤Ēā¤°ā¤ž. \".tif\" ā¤¯ā¤ž ā¤¸ā¤žā¤Žā¤žā¤¨āĨā¤¯ ā¤Ēā¤Ĩā¤žā¤ĩ⤰ ā¤¸ā¤Žā¤žā¤ĒāĨā¤¤ ⤅⤏⤞āĨ‡ā¤˛āĨā¤¯ā¤ž ⤏⤰āĨā¤ĩ ā¤–ā¤¤ā¤žā¤ĩ⤪āĨā¤¯ā¤ž ā¤ĻāĨā¤°āĨā¤˛ā¤•āĨā¤ˇāĨ€ā¤¤ ⤕⤰⤪āĨā¤¯ā¤žā¤¸ā¤žā¤ āĨ€ \"**/.tif\" ā¤ĩā¤žā¤Ēā¤°ā¤ž. ā¤ĩā¤ŋā¤ļā¤ŋ⤎āĨā¤Ÿ ā¤Ēā¤Ĩ ā¤ĻāĨā¤°āĨā¤˛ā¤•āĨā¤ˇ ⤕⤰⤪āĨā¤¯ā¤žā¤¸ā¤žā¤ āĨ€ \"/path/to/ignore/**\" ā¤ĩā¤žā¤Ēā¤°ā¤ž.", + "admin_user": "ā¤ĒāĨā¤°ā¤ļā¤žā¤¸ā¤¨ ā¤ĩā¤žā¤Ē⤰⤕⤰āĨā¤¤ā¤ž", "asset_offline_description": "ā¤šāĨ€ ā¤Ŧā¤žā¤šāĨā¤¯ ⤏⤂⤗āĨā¤°ā¤šā¤žā¤˛ā¤¯ ā¤¸ā¤‚ā¤¸ā¤žā¤§ā¤¨āĨ‡ ā¤Ąā¤ŋ⤏āĨā¤•ā¤ĩ⤰ ā¤¨ā¤žā¤šāĨ€ā¤¤ ⤆⤪ā¤ŋ ⤟āĨā¤°āĨ…ā¤ļā¤Žā¤§āĨā¤¯āĨ‡ ā¤ĩā¤ŋ⤏āĨā¤Ĩā¤žā¤Ēā¤ŋ⤤ ⤕āĨ‡ā¤˛āĨ€ ⤗āĨ‡ā¤˛āĨ€ ā¤†ā¤šāĨ‡ā¤¤. ⤜⤰ ā¤Ģā¤žā¤‡ā¤˛ ⤏⤂⤗āĨā¤°ā¤šā¤žā¤˛ā¤¯ā¤žā¤Žā¤§āĨā¤¯āĨ‡ ā¤ĩā¤ŋ⤏āĨā¤Ĩā¤žā¤Ēā¤ŋ⤤ ⤕āĨ‡ā¤˛āĨ€ ⤗āĨ‡ā¤˛āĨ€ ā¤†ā¤šāĨ‡, ⤤⤰ ⤍ā¤ĩāĨ€ā¤¨ ⤏⤂⤗⤤ ā¤¸ā¤‚ā¤¸ā¤žā¤§ā¤¨ ⤕ā¤ŋ⤂ā¤ĩāĨā¤šā¤ž ⤰āĨ‹ā¤œāĨ€ā¤¨ā¤ŋā¤ļāĨ€ ā¤Žā¤§āĨā¤¯āĨ‡ ⤤ā¤Ēā¤žā¤¸ā¤ž. ā¤šā¤ž ā¤¸ā¤‚ā¤¸ā¤žā¤§ā¤¨ ā¤ĩā¤žā¤Ē⤰ ⤕⤰⤪āĨā¤¯ā¤žā¤¸ā¤žā¤ āĨ€ ⤕āĨƒā¤Ēā¤¯ā¤ž ⤍ā¤ŋā¤ŽāĨā¤¨ā¤˛ā¤ŋ⤖ā¤ŋ⤤ ā¤–ā¤¤ā¤žā¤ĩ⤪āĨ€ ā¤Ēā¤Ĩā¤žā¤˛ā¤ž ā¤‡ā¤ŽāĨā¤ŽāĨ€ā¤š ā¤ĻāĨā¤ĩā¤žā¤°ā¤ž ā¤ĩā¤žā¤Ē⤰āĨ‚ ā¤ļ⤕⤤āĨ‹ ā¤¯ā¤žā¤šāĨ€ ⤤ā¤Ēā¤žā¤¸ā¤ŖāĨ€ ā¤•ā¤°ā¤ž ⤆⤪ā¤ŋ ⤤āĨ‹ ⤏⤂⤗āĨā¤°ā¤šā¤žā¤˛ā¤¯ ā¤šā¤žā¤ŗā¤ž.", "authentication_settings": "ā¤ĒāĨā¤°ā¤Žā¤žā¤ŖāĨ€ā¤•⤰⤪ ā¤¸ā¤žā¤§ā¤•", "authentication_settings_description": "ā¤Ē⤰ā¤ĩ⤞āĨ€ā¤šā¤ž ā¤ļā¤ŦāĨā¤Ļ, OAuth ⤆⤪ā¤ŋ ⤅⤍āĨā¤¯ ā¤ĒāĨā¤°ā¤Žā¤žā¤ŖāĨ€ā¤•⤰⤪ ā¤ĒāĨā¤°ā¤Ŧ⤂⤧⤍ ā¤•ā¤°ā¤ž", @@ -47,6 +53,7 @@ "confirm_email_below": "ā¤ĒāĨā¤ˇāĨā¤ŸāĨ€ ⤕⤰⤪āĨā¤¯ā¤ž ā¤¸ā¤žā¤ āĨ€, ā¤–ā¤žā¤˛āĨ€ \"{email}\" ā¤Ÿā¤‚ā¤•ā¤˛ā¤ŋ⤖ā¤ŋ⤤ ā¤•ā¤°ā¤ž", "confirm_reprocess_all_faces": "⤤āĨā¤ŽāĨā¤šā¤žā¤˛ā¤ž ā¤–ā¤žā¤¤āĨā¤°āĨ€ ā¤†ā¤šāĨ‡ ā¤•ā¤ž ⤕āĨ€ ⤤āĨā¤ŽāĨā¤šā¤žā¤˛ā¤ž ⤏⤰āĨā¤ĩ ⤚āĨ‡ā¤šā¤ąāĨā¤¯ā¤žā¤‚ā¤ĩ⤰ ā¤ĒāĨā¤¨āĨā¤šā¤ž ā¤ĒāĨā¤°ā¤•āĨā¤°ā¤ŋā¤¯ā¤ž ā¤•ā¤°ā¤žā¤¯ā¤šāĨ€ ā¤†ā¤šāĨ‡? ā¤¯ā¤žā¤ŽāĨā¤ŗāĨ‡ ā¤¨ā¤žā¤ĩ ā¤Ļā¤ŋ⤞āĨ‡ā¤˛āĨ‡ ⤞āĨ‹ā¤•ā¤šāĨ€ ā¤¸ā¤žā¤Ģ ā¤šāĨ‹ā¤¤āĨ€ā¤˛.", "confirm_user_password_reset": "⤤āĨā¤ŽāĨā¤šā¤žā¤˛ā¤ž ⤍⤕āĨā¤•āĨ€ {user} ā¤šā¤ž ā¤Ē⤰ā¤ĩ⤞āĨ€ā¤šā¤ž ā¤ļā¤ŦāĨā¤Ļ ā¤Ŧā¤Ļā¤˛ā¤žā¤¯ā¤šā¤ž ā¤†ā¤šāĨ‡ ā¤•ā¤ž?", + "confirm_user_pin_code_reset": "⤤āĨā¤ŽāĨā¤šā¤žā¤˛ā¤ž ⤍⤕āĨā¤•āĨ€ {user} ā¤šā¤ž ā¤Ēā¤ŋ⤍ ⤕āĨ‹ā¤Ą ⤰āĨ€ā¤¸āĨ‡ā¤Ÿ ā¤•ā¤°ā¤žā¤¯ā¤šā¤ž ā¤†ā¤šāĨ‡ ā¤•ā¤ž?", "create_job": "ā¤•ā¤žā¤°āĨā¤¯ ā¤Ŧ⤍ā¤ĩā¤ž", "cron_expression": "ā¤ĩāĨ‡ā¤ŗā¤žā¤Ē⤤āĨā¤°ā¤• ⤏āĨ‚⤤āĨā¤°", "cron_expression_description": "ā¤šā¤žā¤ŗā¤¨āĨā¤¯ā¤žā¤šāĨ‡ ā¤ĩāĨ‡ā¤ŗā¤žā¤Ē⤤āĨā¤°ā¤• ⤕āĨā¤°āĨ‰ā¤¨ ā¤Ēā¤ĻāĨā¤§ā¤¤āĨ€ ⤍āĨ‡ ā¤•ā¤°ā¤ž. ⤅⤧ā¤ŋ⤕ ā¤Žā¤žā¤šā¤ŋ⤤āĨ€ ā¤¸ā¤žā¤ āĨ€ ā¤Ēā¤šā¤ž: ⤕āĨā¤°āĨ‰ā¤¨ ⤗āĨā¤°āĨ", @@ -55,6 +62,16 @@ "duplicate_detection_job_description": "ā¤¸ā¤žā¤°ā¤–āĨā¤¯ā¤ž ā¤›ā¤žā¤¯ā¤žā¤šā¤ŋ⤤āĨā¤°ā¤žā¤‚ā¤šā¤ž ā¤ļāĨ‹ā¤§ ⤘āĨ‡ā¤ŖāĨā¤¯ā¤žā¤¸ā¤žā¤ āĨ€ ā¤¯ā¤žā¤‚ā¤¤āĨā¤°ā¤ŋ⤕āĨ€ ā¤ĒāĨā¤°ā¤ļā¤ŋ⤕āĨā¤ˇā¤Ŗ ā¤ĻāĨā¤¯ā¤ž. ā¤šāĨ€ ā¤•ā¤žā¤°āĨā¤¯ā¤•āĨā¤ˇā¤Žā¤¤ā¤ž ⤚⤤āĨā¤° ā¤ļāĨ‹ā¤§ā¤ĒāĨā¤°ā¤Ŗā¤žā¤˛āĨ€ā¤ĩ⤰ ⤅ā¤ĩ⤞⤂ā¤ŦāĨ‚⤍ ā¤†ā¤šāĨ‡", "exclusion_pattern_description": "⤆ā¤Ē⤞āĨ‡ ⤏⤂⤗āĨā¤°ā¤šā¤žā¤˛ā¤¯ ā¤šā¤žā¤ŗā¤¤ā¤žā¤¨ā¤ž ⤅ā¤Ēā¤ĩā¤žā¤Ļ ā¤¨ā¤ŽāĨā¤¨āĨ‡ ⤆ā¤Ē⤞āĨā¤¯ā¤žā¤˛ā¤ž ā¤–ā¤¤ā¤žā¤ĩ⤪āĨā¤¯ā¤ž ⤆⤪ā¤ŋ ⤰āĨā¤¨ā¤ŋ⤰āĨā¤ĻāĨ‡ā¤ļā¤ŋ⤕āĨ‡ā¤˛ā¤ž ā¤ĻāĨā¤°āĨā¤˛ā¤•āĨā¤ˇāĨ€ā¤¤ ⤕⤰āĨ‚ ā¤ĻāĨ‡ā¤¤ā¤žā¤¤. ⤆ā¤Ē⤞āĨā¤¯ā¤žā¤•ā¤ĄāĨ‡ ā¤•ā¤šāĨā¤šāĨā¤¯ā¤ž ā¤–ā¤¤ā¤žā¤ĩ⤪āĨā¤¯ā¤ž ā¤¸ā¤žā¤°ā¤–āĨā¤¯ā¤ž ā¤†ā¤¯ā¤žā¤¤ ⤕⤰āĨ‚ ā¤‡ā¤šāĨā¤›ā¤ŋ⤤ ⤍⤏⤞āĨ‡ā¤˛āĨā¤¯ā¤ž ⤅⤏⤂ā¤Ēā¤žā¤Ļā¤ŋ⤤ (RAW) ā¤–ā¤¤ā¤žā¤ĩ⤪āĨā¤¯ā¤ž ⤅⤏⤞āĨ‡ā¤˛āĨā¤¯ā¤ž ⤍ā¤ŋ⤰āĨā¤ĻāĨ‡ā¤ļā¤ŋā¤•ā¤ž ⤅⤏⤞āĨā¤¯ā¤žā¤¸ ā¤šāĨ‡ ⤉ā¤Ē⤝āĨā¤•āĨā¤¤ ā¤†ā¤šāĨ‡.", "external_library_management": "ā¤Ŧā¤žā¤šāĨā¤¯ ⤏⤂⤗āĨā¤°ā¤šā¤žā¤˛ā¤¯ ā¤ĩāĨā¤¯ā¤ĩ⤏āĨā¤Ĩā¤žā¤Ē⤍", - "face_detection": "ā¤ŽāĨā¤– ⤏⤂ā¤ļāĨ‹ā¤§ā¤¨" + "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_fullsize_enabled": "ā¤ĒāĨ‚⤰āĨā¤Ŗ-ā¤†ā¤•ā¤žā¤°ā¤žā¤¤āĨ€ā¤˛ ā¤ĒāĨā¤°ā¤¤ā¤ŋā¤Žā¤ž ⤍ā¤ŋ⤰āĨā¤Žā¤ŋ⤤āĨ€", + "image_fullsize_enabled_description": "ā¤ĩāĨ‡ā¤Ŧ-ā¤ĢāĨā¤°āĨ‡ā¤‚ā¤Ąā¤˛āĨ€ ⤍⤏⤞āĨ‡ā¤˛āĨā¤¯ā¤ž ā¤ĢāĨ‰ā¤°ā¤ŽāĨ…ā¤Ÿā¤¸ā¤žā¤ āĨ€ ā¤ĒāĨ‚⤰āĨā¤Ŗ-ā¤†ā¤•ā¤žā¤°ā¤žā¤šāĨ€ ā¤ĒāĨā¤°ā¤¤ā¤ŋā¤Žā¤ž ā¤¤ā¤¯ā¤žā¤° ā¤•ā¤°ā¤ž. ⤜āĨ‡ā¤ĩāĨā¤šā¤ž \"embedded preview\" ā¤šā¤žā¤˛āĨā¤†ā¤¸āĨ‡ā¤˛ ⤤āĨ‡ā¤ĩāĨā¤šā¤ž, \"embedded preview\" ā¤ĨāĨ‡ā¤Ÿ ⤰āĨ‚ā¤Ēā¤žā¤‚ā¤¤ā¤°ā¤Ŗā¤žā¤ļā¤ŋā¤ĩā¤žā¤¯ ā¤ĩā¤žā¤Ē⤰⤞āĨ‡ ā¤œā¤žā¤¤ā¤žā¤¤. JPEG ā¤¸ā¤žā¤°ā¤–āĨā¤¯ā¤ž ā¤ĩāĨ‡ā¤Ŧ-ā¤ĢāĨā¤°āĨ‡ā¤‚ā¤Ąā¤˛āĨ€ ā¤ĢāĨ‰ā¤°ā¤ŽāĨ…ā¤Ÿā¤ĩ⤰ ā¤Ē⤰ā¤ŋā¤Ŗā¤žā¤Ž ā¤šāĨ‹ā¤¤ ā¤¨ā¤žā¤šāĨ€.", + "image_fullsize_quality_description": "āĨ§-āĨ§āĨĻāĨĻ ā¤Ē⤰āĨā¤¯ā¤‚⤤ ā¤ĒāĨ‚⤰āĨā¤Ŗ-ā¤†ā¤•ā¤žā¤°ā¤žā¤¤āĨ€ā¤˛ ā¤ĒāĨā¤°ā¤¤ā¤ŋā¤Žā¤ž ⤗āĨā¤Ŗā¤ĩ⤤āĨā¤¤ā¤ž. ā¤œā¤žā¤¸āĨā¤¤ ⤤āĨ‡ā¤ĩāĨā¤šā¤ĄāĨ‡ ā¤šā¤žā¤‚ā¤—ā¤˛āĨ‡, ā¤Ē⤰⤂⤤āĨ ā¤ŽāĨ‹ā¤ āĨā¤¯ā¤ž ā¤Ģā¤žā¤¯ā¤˛āĨ€ ā¤¤ā¤¯ā¤žā¤° ⤕⤰⤤āĨ‡." } } diff --git a/i18n/ms.json b/i18n/ms.json index cfe935102e..e7e0432a4c 100644 --- a/i18n/ms.json +++ b/i18n/ms.json @@ -14,6 +14,7 @@ "add_a_location": "Tambah lokasi", "add_a_name": "Tambah nama", "add_a_title": "Tambah tajuk", + "add_endpoint": "Tambah titik akhir", "add_exclusion_pattern": "Tambahkan corak pengecualian", "add_import_path": "Tambahkan laluan import", "add_location": "Tambah lokasi", @@ -21,6 +22,7 @@ "add_partner": "Tambah rakan", "add_path": "Tambah laluan", "add_photos": "Tambah gambar", + "add_tag": "Tambah tag", "add_to": "Tambah keâ€Ļ", "add_to_album": "Tambah ke album", "add_to_album_bottom_sheet_added": "Dimasukkan ke {album}", @@ -32,17 +34,18 @@ "added_to_favorites_count": "Menambahkan {count, number} ke kegemaran", "admin": { "add_exclusion_pattern_description": "Tambahkan corak pengecualian. Globbing menggunakan *, **, dan ? disokong. Untuk mengabaikan semua fail dalam mana-mana direktori bernama \"Raw\", gunakan \"**/Raw/**\". Untuk mengabaikan semua fail yang berakhir dengan \".tif\", gunakan \"**/*.tif\". Untuk mengabaikan laluan mutlak, gunakan \"/path/to/ignore/**\".", + "admin_user": "Pengguna Pentadbir", "asset_offline_description": "Aset pustaka luaran ini tidak lagi ditemui pada cakera dan telah dialihkan ke sampah. Jika fail telah dialihkan dalam pustaka, semak garis masa anda untuk aset baharu yang sepadan. Untuk memulihkan aset ini, sila pastikan bahawa laluan fail di bawah boleh diakses oleh Immich dan mengimbas pustaka.", "authentication_settings": "Tetapan Pengesahan", "authentication_settings_description": "Urus kata laluan, OAuth dan tetapan pengesahan lain", "authentication_settings_disable_all": "Adakah anda pasti mahu melumpuhkan semua kaedah log masuk? Log masuk akan dilumpuhkan sepenuhnya.", "authentication_settings_reenable": "Untuk menghidupkan semula, guna Arahan Pelayan.", "background_task_job": "Tugas Latar Belakang", - "backup_database": "Sandar pangkalan data", - "backup_database_enable_description": "Aktifkan sandaran pangkalan data", - "backup_keep_last_amount": "Jumlah sandaran sebelumnya yang hendak disimpan", - "backup_settings": "Tetapan Sandaran", - "backup_settings_description": "Urus tetapan sandaran pangkalan data", + "backup_database": "Buat Salinan Pangkalan Data", + "backup_database_enable_description": "Dayakan salinan pangkalan data", + "backup_keep_last_amount": "Jumlah salinan pangkalan data sebelumnya untuk disimpan", + "backup_settings": "Tetapan Salinan Pangkalan Data", + "backup_settings_description": "Urus tetapan salinan pangkalan data.", "cleared_jobs": "Kerja telah dibersihkan untuk: {job}", "config_set_by_file": "Konfigurasi kini ditetapkan oleh fail konfigurasi", "confirm_delete_library": "Adakah anda pasti mahu memadamkan {library}?", @@ -72,7 +75,7 @@ "image_fullsize_quality_description": "Kualiti imej bersaiz penuh dari 1-100. Lebih tinggi adalah lebih baik, tetapi menghasilkan fail yang lebih besar.", "image_fullsize_title": "Tetapan Imej bersaiz penuh", "image_prefer_embedded_preview": "Cadangkan pratonton terbenam", - "image_prefer_embedded_preview_setting_description": "Gunakan pratonton terbenam dalam foto RAW sebagai input kepada pemprosesan imej apabila tersedia. Cara ini boleh menghasilkan warna yang lebih tepat untuk sesetengah imej, tetapi kualiti pratonton bergantung pada kamera dan imej mungkin mempunyai lebih banyak artifak mampatan.", + "image_prefer_embedded_preview_setting_description": "Gunakan pratonton terbenam dalam foto RAW sebagai input untuk pemprosesan imej apabila tersedia. Ini boleh menghasilkan warna yang lebih tepat untuk sesetengah imej, tetapi kualiti pratonton bergantung kepada kamera dan imej mungkin mengandungi lebih banyak artifak pemampatan.", "image_prefer_wide_gamut": "Cadangkan warna gamut yang luas", "image_prefer_wide_gamut_setting_description": "Gunakan Paparan P3 untuk lakaran kenit. Ini lebih baik mengekalkan kerancakan imej dengan ruang warna yang luas, tetapi imej mungkin kelihatan berbeza pada peranti lama dengan versi penyemak imbas lama. Imej sRGB disimpan sebagai sRGB untuk mengelakkan peralihan warna.", "image_preview_description": "Imej bersaiz sederhana dengan metadata yang dilucutkan, digunakan semasa melihat aset tunggal dan untuk pembelajaran mesin", @@ -102,7 +105,7 @@ "library_scanning_enable_description": "Dayakan pengimbasan perpustakaan berkala", "library_settings": "Perpustakaan Luaran", "library_settings_description": "Urus tetapan perpustakaan luaran", - "library_tasks_description": "Laksanakan tugas perpustakaan", + "library_tasks_description": "Imbas pustaka luaran untuk aset yang baru dan/atau telah diubah", "library_watching_enable_description": "Perhatikan perpustakaan luaran untuk perubahan fail", "library_watching_settings": "Perhati perpustakaan (EKSPERIMEN)", "library_watching_settings_description": "Perhati fail yang diubah secara automatik", @@ -137,7 +140,7 @@ "machine_learning_smart_search_description": "Cari imej secara semantik menggunakan pembenaman CLIP", "machine_learning_smart_search_enabled": "Dayakan carian pintar", "machine_learning_smart_search_enabled_description": "Jika ditutup, gambar-gambar tidak akan dikodkan untuk carian pintar.", - "machine_learning_url_description": "URL pelayan pembelajaran mesin. Jika lebih daripada satu URL disediakan, setiap pelayan akan dicuba satu demi satu sehingga satu menjawab dengan jayanya, mengikut urutan dari pertama hingga terakhir.", + "machine_learning_url_description": "URL pelayan pembelajaran mesin. Jika lebih daripada satu URL disediakan, setiap pelayan akan dicuba satu demi satu mengikut turutan, dari yang pertama hingga yang terakhir, sehingga salah satu memberi maklum balas yang berjaya. Pelayan yang tidak memberi maklum balas akan diabaikan sementara sehingga ia kembali dalam talian.", "manage_concurrency": "Urus Concurrency", "manage_log_settings": "Urus tetapan log", "map_dark_style": "Tema gelap", @@ -146,13 +149,15 @@ "map_gps_settings_description": "Urus Tetapan Peta & GPS (Geokod Terbalik)", "map_implications": "Ciri peta bergantung pada perkhidmatan jubin luaran (tiles.immich.cloud)", "map_light_style": "Tema terang", - "map_manage_reverse_geocoding_settings": "Urus tetapan Geocoding Songsang", + "map_manage_reverse_geocoding_settings": "Urus tetapan Penentuan Alamat Songsang", "map_reverse_geocoding": "Geokoding Sonsang", "map_reverse_geocoding_enable_description": "Dayakan pengekodan geo terbalik", "map_reverse_geocoding_settings": "Tetapan Pengekodan Geo Terbalik", "map_settings": "Peta", "map_settings_description": "Urus tetapan peta", "map_style_description": "URL ke tema peta style.json", + "memory_cleanup_job": "Pembersihan memori", + "memory_generate_job": "Penjanaan memori", "metadata_extraction_job": "Sari metadata", "metadata_extraction_job_description": "Sari maklumat metadata dari setiap aset, seperti GPS, muka-muka, dan pelaraian", "metadata_faces_import_setting": "Dayakan import muka", @@ -161,12 +166,26 @@ "metadata_settings_description": "Urus tetapan metadata", "migration_job": "Migrasi", "migration_job_description": "Pindahkan imej kecil untuk aset-aset dan muka-muka kepada struktur folder terkini", + "nightly_tasks_cluster_faces_setting_description": "Jalankan pengecaman wajah kepada wajah baharu yang dijumpai", + "nightly_tasks_cluster_new_faces_setting": "Kumpulan wajah baharu", + "nightly_tasks_database_cleanup_setting": "Tugasan membersihkan pangkalan data", + "nightly_tasks_database_cleanup_setting_description": "Membersihkan data lama, luput dari pangkalan data", + "nightly_tasks_generate_memories_setting": "Menjana memori", + "nightly_tasks_generate_memories_setting_description": "Mencipta memori dari aset", + "nightly_tasks_missing_thumbnails_setting": "Menjana lakaran kecil yang hilang", + "nightly_tasks_missing_thumbnails_setting_description": "Aturan aset tanpa lakaran kecil untuk janaan lakaran kecil", + "nightly_tasks_settings": "Tetapan tugasan malam", + "nightly_tasks_settings_description": "Mengurus tugasan malam", + "nightly_tasks_start_time_setting": "Masa mula", + "nightly_tasks_start_time_setting_description": "Masa di mana pelayan mula bekerja pada tugasan malam", + "nightly_tasks_sync_quota_usage_setting": "Penyelarasan penggunaan kuota", + "nightly_tasks_sync_quota_usage_setting_description": "Kemaskini kuota simpanan pengguna, berdasarkan kepada penggunaan terkini", "no_paths_added": "Tiada laluan yang ditambah", "no_pattern_added": "Tiada corak ditambah", "note_apply_storage_label_previous_assets": "Nota: Untuk menggunakan Label Storan pada aset yang dimuat naik sebelum ini, jalankan", "note_cannot_be_changed_later": "NOTA: Ini tidak boleh diubah kemudian!", "notification_email_from_address": "Dari alamat", - "notification_email_from_address_description": "Alamat e-mel penghantar, sebagai contoh: \"Immich Photo Server \"", + "notification_email_from_address_description": "Alamat e-mel penghantar, sebagai contoh: \"Pelayan Gambar Immich \". Pastikan menggunakan alamat yang dibenarkan anda untuk menghantar e-mel.", "notification_email_host_description": "Hos e-mel pelayan (cth. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Abaikan ralat-ralat sijil", "notification_email_ignore_certificate_errors_description": "Abaikan ralat pengesahan sijil TLS (tidak disyorkan)", @@ -186,19 +205,24 @@ "oauth_auto_register": "Daftar secara automatik", "oauth_auto_register_description": "Daftar secara automatik pengguna-pengguna baharu selepas mendaftar masuk dengan OAuth", "oauth_button_text": "Teks butang", + "oauth_client_secret_description": "Diperlukan jika PKCE (Proof Key for Code Exchange) tidak disokong oleh penyedia OAuth", "oauth_enable_description": "Log masuk dengan OAuth", "oauth_mobile_redirect_uri": "URI ubah hala mudah alih", "oauth_mobile_redirect_uri_override": "Penggantian URI ubah hala mudah alih", "oauth_mobile_redirect_uri_override_description": "Aktifkan apabila pembekal OAuth tidak membenarkan URI mudah alih, seperti ''{callback}''", + "oauth_role_claim": "Tebus peranan", + "oauth_role_claim_description": "Automatik memberi kebenaran pentadbir berdasarkan tuntutan ini. Tuntutan ini mungkin mempunyai sama ada 'pengguna' atau 'pentadbir'.", "oauth_settings": "OAuth", - "oauth_settings_description": "Urus tetapan-tetapan log masuk OAuth", + "oauth_settings_description": "Urus tetapan log masuk OAuth", "oauth_settings_more_details": "Untuk maklumat lanjut tentang ciri ini, rujuk ke dokumen.", "oauth_storage_label_claim": "Tuntutan label storan", "oauth_storage_label_claim_description": "Tetapkan label storan pengguna secara automatik kepada nilai tuntutan ini.", "oauth_storage_quota_claim": "Tuntutan kuota storan", "oauth_storage_quota_claim_description": "Tetapkan kuota storan pengguna secara automatik kepada nilai tuntutan ini.", "oauth_storage_quota_default": "Kuota storan lalai (GiB)", - "oauth_storage_quota_default_description": "Kuota dalam GiB untuk digunakan apabila tiada tuntutan disediakan (Masukkan 0 untuk kuota tanpa had).", + "oauth_storage_quota_default_description": "Kuota dalam GiB yang akan digunakan jika tiada tuntutan disediakan.", + "oauth_timeout": "Had Masa Permintaan", + "oauth_timeout_description": "Had masa untuk permintaan dalam milisaat", "password_enable_description": "Log masuk dengan e-mel dan kata laluan", "password_settings": "Kata Laluan Log Masuk", "password_settings_description": "Urus tetapan-tetapan kata laluan log masuk", @@ -207,12 +231,12 @@ "quota_size_gib": "Saiz Kuota (GiB)", "refreshing_all_libraries": "Menyegarkan semua perpustakaan", "registration": "Pendaftaran Pentadbir", - "registration_description": "Memandangkan anda adalah pengguna pertama pada sistem, anda akan ditugaskan sebagai Admin dan bertanggungjawab untuk tugas pentadbiran, serta pengguna tambahan yang akan anda tambah.", + "registration_description": "Memandangkan anda adalah pengguna pertama pada sistem, anda akan ditugaskan sebagai Pentadbir dan bertanggungjawab untuk tugas pentadbiran, serta pengguna tambahan yang akan anda tambah.", "require_password_change_on_login": "Perlukan pengguna menukar kata laluan ketika log masuk pertama", "reset_settings_to_default": "Tetapkan semula tetapan kepada lalai", "reset_settings_to_recent_saved": "Tetapkan semula tetapan kepada tetapan yang disimpan baru-baru ini", "scanning_library": "Mengimbas perpustakaan", - "search_jobs": "Cari kerjaâ€Ļ", + "search_jobs": "Cari tugasanâ€Ļ", "send_welcome_email": "Hantar e-mel alu-aluan", "server_external_domain_settings": "Domain luaran", "server_external_domain_settings_description": "Domain untuk pautan kongsi awam, termasuk http(s)://", @@ -222,7 +246,7 @@ "server_settings_description": "Urus tetapan pelayan", "server_welcome_message": "Mesej alu-aluan", "server_welcome_message_description": "Mesej yang dipaparkan pada halaman log masuk.", - "sidecar_job": "Metadata kereta sisi", + "sidecar_job": "Metadata sampingan", "sidecar_job_description": "Temui atau segerakkan metadata sampingan daripada sistem fail", "slideshow_duration_description": "Bilangan saat untuk memaparkan setiap imej", "smart_search_job_description": "Jalankan pembelajaran mesin pada aset-aset untuk menyokong carian pintar", @@ -233,9 +257,10 @@ "storage_template_hash_verification_enabled_description": "Mendayakan pengesahan hac, jangan lumpuhkan melainkan anda pasti akan implikasinya", "storage_template_migration": "Penghijrahan templat storan", "storage_template_migration_description": "Gunakan {template} semasa pada aset-aset yang dimuat naik sebelum ini", - "storage_template_migration_info": "Perubahan templat hanya akan digunakan pada aset baharu. Untuk menggunakan templat secara retroaktif pada aset-aset yang dimuat naik sebelum ini, jalankan {job}.", + "storage_template_migration_info": "Templat storan akan menukar semua sambungan fail kepada huruf kecil. Perubahan templat hanya akan digunakan untuk aset baru. Untuk menggunakan templat ini secara retroaktif pada aset yang telah dimuat naik sebelum ini, jalankan {job}.", "storage_template_migration_job": "Kerja Migrasi Templat Storan", "storage_template_more_details": "Untuk butiran lanjut tentang ciri ini, rujuk kepada Templat Storan dan implikasi", + "storage_template_onboarding_description_v2": "Apabila diaktifkan, ciri ini akan mengatur fail secara automatik berdasarkan templat yang ditetapkan oleh pengguna. Untuk maklumat lanjut, sila rujuk dokumentasi.", "storage_template_path_length": "Anggaran kepanjangan laluan: {length, number}/{limit, number}", "storage_template_settings": "Templat Storan", "storage_template_settings_description": "Urus struktur folder dan nama fail aset dimuat naik", @@ -250,7 +275,7 @@ "template_email_update_album": "Templat Kemas kini Album", "template_email_welcome": "Templat e-mel alu-aluan", "template_settings": "Templat Pemberitahuan", - "template_settings_description": "Urus templat tersuai untuk pemberitahuan.", + "template_settings_description": "Urus templat tersuai untuk notifikasi", "theme_custom_css_settings": "CSS tersuai", "theme_custom_css_settings_description": "Lembaran Gaya Lata membolehkan reka bentuk Immich disuaikan.", "theme_settings": "Tetapan Tema", @@ -282,7 +307,7 @@ "transcoding_encoding_options": "Pilihan Pengekodan", "transcoding_encoding_options_description": "Tetapkan codec, resolusi, kualiti dan pilihan lain untuk video yang dikodkan", "transcoding_hardware_acceleration": "Pecutan Perkakasan", - "transcoding_hardware_acceleration_description": "Eksperimen; lebih pantas, tetapi akan mempunyai kualiti yang lebih rendah pada kadar bit yang sama", + "transcoding_hardware_acceleration_description": "Eksperimen: pengekodan semula yang lebih pantas tetapi mungkin mengurangkan kualiti pada kadar bit yang sama", "transcoding_hardware_decoding": "Penyahkodan perkakasan", "transcoding_hardware_decoding_setting_description": "Mendayakan pecutan hujung ke hujung dan bukannya hanya mempercepatkan pengekodan. Mungkin tidak berfungsi pada semua video.", "transcoding_max_b_frames": "Bingkai-B maksimum", @@ -311,8 +336,61 @@ "transcoding_threads_description": "Nilai yang lebih tinggi membawa kepada pengekodan yang lebih pantas, tetapi meninggalkan lebih sedikit ruang untuk pemproses tugas lain semasa aktif. Nilai ini tidak boleh lebih daripada bilangan teras CPU. Memaksimumkan penggunaan jika ditetapkan kepada 0.", "transcoding_tone_mapping": "Pemetaan nada", "transcoding_tone_mapping_description": "Percubaan untuk mengekalkan penampilan video HDR apabila ditukar kepada SDR. Setiap algoritma membuat pertukaran yang berbeza untuk warna, perincian dan kecerahan. Hable mengekalkan perincian, Mobius mengekalkan warna, dan Reinhard mengekalkan kecerahan.", - "transcoding_transcode_policy": "Dasar transkod" + "transcoding_transcode_policy": "Dasar transkod", + "transcoding_transcode_policy_description": "Dasar untuk bila video perlu ditranskod. Video HDR akan sentiasa ditranskod (kecuali jika pengekodan semula dinyahdayakan).", + "transcoding_two_pass_encoding": "Pengekodan dua lelaran", + "transcoding_two_pass_encoding_setting_description": "Transkod dalam dua lelaran untuk menghasilkan video yang ditranskod dengan kualiti lebih baik. Apabila kadar bit maksimum diaktifkan (diperlukan untuk berfungsi dengan H.264 dan HEVC), mod ini akan menggunakan julat kadar bit berdasarkan kadar bit maksimum dan mengabaikan CRF. Untuk VP9, CRF boleh digunakan jika kadar bit maksimum dinyahdayakan.", + "transcoding_video_codec": "Kodek video", + "transcoding_video_codec_description": "VP9 mempunyai kecekapan tinggi dan keserasian web yang baik, tetapi mengambil masa lebih lama untuk ditranskod. HEVC memberikan prestasi yang serupa, tetapi kurang serasi dengan web. H.264 sangat serasi dan pantas untuk ditranskod, tetapi menghasilkan fail yang jauh lebih besar. AV1 ialah kodek paling cekap tetapi tidak disokong pada peranti lama.", + "trash_enabled_description": "Dayakan ciri Tong Sampah", + "trash_number_of_days": "Bilangan hari", + "trash_number_of_days_description": "Bilangan hari untuk menyimpan aset dalam tong sampah sebelum dipadam secara kekal", + "trash_settings": "Tetapan Tong Sampah", + "trash_settings_description": "Urus tetapan tong sampah", + "user_cleanup_job": "Pembersihan pengguna", + "user_delete_delay": "Akaun dan aset {user} akan dijadualkan untuk dipadam secara kekal dalam {delay, plural, one {# hari} other {# hari}}.", + "user_delete_delay_settings": "Kelewatan pemadaman", + "user_delete_delay_settings_description": "Bilangan hari selepas penghapusan sebelum akaun dan aset pengguna dipadam secara kekal. Tugasan pemadaman pengguna dijalankan pada tengah malam untuk menyemak pengguna yang sedia untuk dipadam. Perubahan pada tetapan ini akan dinilai semasa pelaksanaan seterusnya.", + "user_delete_immediately": "Akaun dan aset {user} akan dimasukkan ke dalam baris gilir untuk dipadam secara kekal serta-merta.", + "user_delete_immediately_checkbox": "Masukkan pengguna dan aset ke dalam baris gilir untuk dipadam serta-merta", + "user_details": "Butiran Pengguna", + "user_management": "Pengurusan Pengguna", + "user_password_has_been_reset": "Katalaluan pengguna telah ditetapkan semula:", + "user_password_reset_description": "Sila berikan katalaluan sementara kepada pengguna dan maklumkan bahawa mereka perlu menukar katalaluan semasa log masuk yang seterusnya.", + "user_restore_description": "Akaun {user} akan dipulihkan.", + "user_restore_scheduled_removal": "Pulihkan pengguna – pemadaman dijadualkan pada {date, date, long}", + "user_settings": "Tetapan Pengguna", + "user_settings_description": "Urus tetapan pengguna", + "user_successfully_removed": "Pengguna {email} telah berjaya dipadam.", + "version_check_enabled_description": "Dayakan semakan versi", + "version_check_implications": "Ciri semakan versi bergantung kepada komunikasi berkala dengan github.com", + "version_check_settings": "Semakan Versi", + "version_check_settings_description": "Dayakan/nyahdayakan notifikasi versi baharu", + "video_conversion_job": "Transkod video", + "video_conversion_job_description": "Transkod video untuk keserasian yang lebih luas dengan pelayar dan peranti" }, + "admin_email": "Emel Pentadbir", + "admin_password": "Kata laluan Pentadbir", + "administration": "Pentadbiran", + "advanced": "Lanjutan", + "advanced_settings_beta_timeline_subtitle": "Cuba pengalaman aplikasi baharu", + "advanced_settings_beta_timeline_title": "Garis masa beta", + "advanced_settings_enable_alternate_media_filter_subtitle": "Gunakan pilihan ini untuk menapis media semasa penyegerakan berdasarkan kriteria alternatif. Hanya cuba jika anda menghadapi masalah dengan aplikasi mengesan semua album.", + "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTAL] Gunakan penapis penyelarasan album peranti alternatif", + "advanced_settings_log_level_title": "Tahap log: {level}", + "advanced_settings_prefer_remote_subtitle": "Sesetengah peranti sangat perlahan untuk memuatkan imej kecil daripada aset lokal. Aktifkan tetapan ini untuk memuatkan imej dari jauh sebagai gantinya.", + "advanced_settings_prefer_remote_title": "Utamakan imej jauh", + "advanced_settings_proxy_headers_subtitle": "Tentukan pengepala proksi yang perlu dihantar oleh Immich dengan setiap permintaan rangkaian", + "advanced_settings_proxy_headers_title": "Pengepala Proksi", + "advanced_settings_self_signed_ssl_subtitle": "Langkau pengesahan sijil SSL untuk titik hujung pelayan. Diperlukan untuk sijil yang ditandatangani sendiri.", + "advanced_settings_self_signed_ssl_title": "Benarkan sijil SSL yang ditandatangani sendiri", + "advanced_settings_sync_remote_deletions_subtitle": "Automatik memadam atau memulihkan satu asset di peranti ini apabila tindakan itu diambil di dalam laman sesawang", + "advanced_settings_sync_remote_deletions_title": "Selaraskan pemadaman kawalan jauh [UJI KAJI]", + "advanced_settings_tile_subtitle": "Tetapan lanjutan pengguna", + "advanced_settings_troubleshooting_subtitle": "Dayakan ciri tambahan untuk menyelesaikan masalah", + "advanced_settings_troubleshooting_title": "Menyelesaikan masalah", + "age_months": "Umur {bulan, plural, satu {# bulan} lain {# bulan}}", + "age_year_months": "Umur 1 tahun, {bulan, plural, satu {# bulan} lain {# bulan}}", "deduplication_criteria_1": "Saiz imej dalam bait", "deduplication_criteria_2": "Kiraan data EXIF", "deduplication_info": "Maklumat Pendeduplikasian", @@ -361,5 +439,6 @@ "year": "Tahun", "yes": "Ya", "you_dont_have_any_shared_links": "Anda tidak mempunyai apa-apa pautan yang dikongsi", + "your_wifi_name": "Nama Wi-Fi anda", "zoom_image": "Zum Gambar" } diff --git a/i18n/nb_NO.json b/i18n/nb_NO.json index 3478262019..2abc08e7f2 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -22,8 +22,8 @@ "add_partner": "Legg til partner", "add_path": "Legg til sti", "add_photos": "Legg til bilder", - "add_tag": "Legg til tag", - "add_to": "Legg tilâ€Ļ", + "add_tag": "Legg til tagg", + "add_to": "Legg til iâ€Ļ", "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}", @@ -35,7 +35,7 @@ "admin": { "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": "Denne eksterne bibliotekressursen finnes ikke lenger pÃĨ disk og har blitt flyttet til papirkurven. Hvis filen ble flyttet innad i biblioteket, sjekk tidslinjen din for den tilsvarende ressursen. For ÃĨ gjenopprette ressursen, vennligst sørg for at filstien under er tilgjengelig for Immich og skan biblioteket.", + "asset_offline_description": "Dette eksterne biblioteksobjektet finnes ikke lenger pÃĨ disk og har blitt flyttet til papirkurven. Hvis filen ble flyttet innad i biblioteket, se etter det tilsvarende objektet i tidslinjen din. For ÃĨ gjenopprette objektet, vennligst sørg for at filstien under er tilgjengelig for Immich og skann biblioteket.", "authentication_settings": "Godkjenningsinnstillinger", "authentication_settings_description": "Administrer passord, OAuth, og andre innstillinger for autentisering", "authentication_settings_disable_all": "Er du sikker pÃĨ at du ønsker ÃĨ deaktivere alle innloggingsmetoder? Innlogging vil bli fullstendig deaktivert.", @@ -114,7 +114,7 @@ "logging_settings": "Logger", "machine_learning_clip_model": "Clip-modell", "machine_learning_clip_model_description": "Navnet pÃĨ en CLIP-modell finnes her. Merk at du mÃĨ kjøre 'Smart Søk'-jobben pÃĨ nytt for alle bilder etter at du har endret modell.", - "machine_learning_duplicate_detection": "Duplikat-deteksjon", + "machine_learning_duplicate_detection": "Duplikatsøk", "machine_learning_duplicate_detection_enabled": "Aktiver duplikatdeteksjon", "machine_learning_duplicate_detection_enabled_description": "Hvis deaktivert: helt identiske filer vil fremdeles de-duplisert.", "machine_learning_duplicate_detection_setting_description": "Bruk CLIP-embeddings for ÃĨ finne sannsynlige duplikater", @@ -166,6 +166,20 @@ "metadata_settings_description": "Administrer metadatainnstillinger", "migration_job": "Migrering", "migration_job_description": "Migrer miniatyrbilder for filer og ansikter til den nyeste mappestrukturen", + "nightly_tasks_cluster_faces_setting_description": "Kjør ansiktsgjenkjenning pÃĨ nylige oppdagede ansikter", + "nightly_tasks_cluster_new_faces_setting": "Grupper nye ansikter", + "nightly_tasks_database_cleanup_setting": "Opprydningsjobber for databasen", + "nightly_tasks_database_cleanup_setting_description": "Rydder opp i gamle, utgÃĨtte data fra databasen", + "nightly_tasks_generate_memories_setting": "Genererer minner", + "nightly_tasks_generate_memories_setting_description": "Generer nye minner fra objekter", + "nightly_tasks_missing_thumbnails_setting": "Generer manglende miniatyrbilder", + "nightly_tasks_missing_thumbnails_setting_description": "Legg til objekter i kø som mangler miniatyrbilder for generering", + "nightly_tasks_settings": "Innstillinger for nattjobber", + "nightly_tasks_settings_description": "Endre pÃĨ nattjobber", + "nightly_tasks_start_time_setting": "Starttid", + "nightly_tasks_start_time_setting_description": "Tiden som serveren starter med nattjobbene", + "nightly_tasks_sync_quota_usage_setting": "Synkroniser kvotebruk", + "nightly_tasks_sync_quota_usage_setting_description": "Oppdater brukerkvote basert pÃĨ nÃĨvÃĻrende bruk", "no_paths_added": "Ingen filstier lagt til", "no_pattern_added": "Ingen mønster lagt til", "note_apply_storage_label_previous_assets": "Merk: For ÃĨ bruke lagringsetiketten pÃĨ tidligere opplastede filer, kjør", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "Mobil omdirigerings-URI", "oauth_mobile_redirect_uri_override": "Mobil omdirigerings-URI overstyring", "oauth_mobile_redirect_uri_override_description": "Aktiver nÃĨr OAuth-leverandøren ikke tillater en mobil URI, som ''{callback}''", + "oauth_role_claim": "Krev Rolle", + "oauth_role_claim_description": "Gi automatisk administratortilgang basert pÃĨ tilstedevÃĻrelsen av dette kravet. Kravet kan ha enten ÂĢbrukerÂģ eller ÂĢadministratorÂģ.", "oauth_settings": "OAuth", "oauth_settings_description": "Administrer innstillinger for OAuth-innlogging", "oauth_settings_more_details": "For mer informasjon om denne funksjonen, se dokumentasjonen.", @@ -212,7 +228,7 @@ "password_settings_description": "Administrer innstillinger for passordinnlogging", "paths_validated_successfully": "Alle filstier validert uten problemer", "person_cleanup_job": "Person opprydding", - "quota_size_gib": "Kvotestørrelse (GiB)", + "quota_size_gib": "Kvote (GiB)", "refreshing_all_libraries": "Oppdaterer alle biblioteker", "registration": "Administrator registrering", "registration_description": "Siden du er den første brukeren pÃĨ systemet, vil du bli utnevnt til administrator og ha ansvar for administrative oppgaver. Du vil ogsÃĨ opprette eventuelle nye brukere.", @@ -236,12 +252,12 @@ "smart_search_job_description": "Kjør maskinlÃĻring pÃĨ filer for ÃĨ støtte smart søk", "storage_template_date_time_description": "Elementets opprettelsestidspunkt brukes for datotid-informasjonen", "storage_template_date_time_sample": "Eksempeltid {date}", - "storage_template_enable_description": "Aktiver lagringstemplatmotoren", + "storage_template_enable_description": "Aktiver lagringsmal-motoren", "storage_template_hash_verification_enabled": "Hash verifisering aktivert", "storage_template_hash_verification_enabled_description": "Aktiver hasjverifisering. Ikke deaktiver dette med mindre du er sikker pÃĨ konsekvensene", - "storage_template_migration": "Lagringsmal migrering", + "storage_template_migration": "Implementer lagringsmal", "storage_template_migration_description": "Bruk gjeldende {template} pÃĨ tidligere opplastede bilder", - "storage_template_migration_info": "Lagringsmalen vil endre filtypen til smÃĨ bokstaver. Malendringer vil kun gjelde nye ressurser. For ÃĨ anvende malen pÃĨ tidligere opplastede ressurser, kjør {job}.", + "storage_template_migration_info": "Lagringsmalen vil endre filtypen til smÃĨ bokstaver. Malendringer vil kun gjelde nye objekter. For ÃĨ anvende malen pÃĨ tidligere opplastede objekter, kjør {job}.", "storage_template_migration_job": "Migreringsjobb for lagringsmal", "storage_template_more_details": "For mer informasjon om denne funksjonen, se lagringsmalen og dens konsekvenser", "storage_template_onboarding_description_v2": "NÃĨr aktivert vil denne funksjonen automatisk organisere filer basert pÃĨ en brukerdefinert mal. For mer informasjon, se denne linken dokumentasjon.", @@ -250,19 +266,19 @@ "storage_template_settings_description": "Administrer mappestrukturen og filnavnet til opplastede fil", "storage_template_user_label": "{label} er brukerens Lagringsetikett", "system_settings": "Systeminstillinger", - "tag_cleanup_job": "Tag opprydding", + "tag_cleanup_job": "Tagg-opprydding", "template_email_available_tags": "Du kan bruke følgende variabler i din mal: {tags}", "template_email_if_empty": "Hvis malen er tom, vil standard epost bli brut.", "template_email_invite_album": "Inviter Album Mal", "template_email_preview": "ForhÃĨndsvis", - "template_email_settings": "Epost mal", + "template_email_settings": "E-postmaler", "template_email_update_album": "Oppdater Album Mal", - "template_email_welcome": "Mal for velkomst epost", + "template_email_welcome": "Mal for velkomst-e-post", "template_settings": "Varslings Mal", "template_settings_description": "Administrer tilpassede maler for varsling", "theme_custom_css_settings": "Egendefinert CSS", "theme_custom_css_settings_description": "Cascading Style Sheets gjør det mulig ÃĨ tilpasse designet av Immich.", - "theme_settings": "Tema innstillinger", + "theme_settings": "Tema-innstillinger", "theme_settings_description": "Administrer tilpasning av Immich webgrensesnitt", "thumbnail_generation_job": "Generer miniatyrbilder", "thumbnail_generation_job_description": "Generer store, smÃĨ og uskarpe miniatyrbilder for hver fil, samt miniatyrbilder for hver person", @@ -357,10 +373,12 @@ "admin_password": "Administrator Passord", "administration": "Administrasjon", "advanced": "Avansert", + "advanced_settings_beta_timeline_subtitle": "Prøv den nye app opplevelsen", + "advanced_settings_beta_timeline_title": "Beta tidslinje", "advanced_settings_enable_alternate_media_filter_subtitle": "Bruk denne innstillingen for ÃĨ filtrere mediefiler under synkronisering basert pÃĨ alternative kriterier. Bruk kun denne innstillingen dersom man opplever problemer med at applikasjonen ikke oppdager alle album.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTELT] Bruk alternativ enhet album synk filter", "advanced_settings_log_level_title": "LoggnivÃĨ: {level}", - "advanced_settings_prefer_remote_subtitle": "Noen enheter er veldige trege til ÃĨ hente mikrobilder fra enheten. Aktiver denne innstillingen for ÃĨ hente de eksternt istedenfor.", + "advanced_settings_prefer_remote_subtitle": "Noen enheter er veldige trege til ÃĨ hente miniatyrbilder fra enheten. Aktiver denne innstillingen for ÃĨ hente de eksternt istedenfor.", "advanced_settings_prefer_remote_title": "Foretrekk eksterne bilder", "advanced_settings_proxy_headers_subtitle": "Definer proxy headere som Immich skal benytte ved enhver nettverksrequest", "advanced_settings_proxy_headers_title": "Proxy headere", @@ -388,6 +406,7 @@ "album_options": "Albumalternativer", "album_remove_user": "Fjerne bruker?", "album_remove_user_confirmation": "Er du sikker pÃĨ at du vil fjerne {user}?", + "album_search_not_found": "Ingen album ble funnet som traff ditt søk", "album_share_no_users": "Ser ut til at du har delt dette albumet med alle brukere, eller du ikke har noen brukere ÃĨ dele det med.", "album_updated": "Album oppdatert", "album_updated_setting_description": "Motta e-postvarsling nÃĨr et delt album fÃĨr nye filer", @@ -407,6 +426,7 @@ "albums_default_sort_order": "Standard sorteringsrekkefølge for albumer", "albums_default_sort_order_description": "Standard sorteringsrekkefølge for bilder nÃĨr man lager et nytt album.", "albums_feature_description": "Samlinger av bilder som kan deles med andre brukere.", + "albums_on_device_count": "Albumer pÃĨ enheten {count}", "all": "Alle", "all_albums": "Alle album", "all_people": "Alle personer", @@ -426,7 +446,8 @@ "app_bar_signout_dialog_title": "Logg ut", "app_settings": "Appinstillinger", "appears_in": "Vises i", - "archive": "Arkiver", + "archive": "Arkiv", + "archive_action_prompt": "{count} lagt til i arkivet", "archive_or_unarchive_photo": "Arkiver eller ta ut av arkivet", "archive_page_no_archived_assets": "Ingen arkiverte objekter funnet", "archive_page_title": "Arkiv ({count})", @@ -464,13 +485,12 @@ "assets": "Filer", "assets_added_count": "Lagt til {count, plural, one {# element} other {# elementer}}", "assets_added_to_album_count": "Lagt til {count, plural, one {# asset} other {# assets}} i album", - "assets_added_to_name_count": "Lagt til {count, plural, one {# asset} other {# assets}} i {hasName, select, true {{name}} other {new album}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} kan ikke legges til i albumet", "assets_count": "{count, plural, one {# fil} other {# filer}}", "assets_deleted_permanently": "{count} objekt(er) slettet permanent", "assets_deleted_permanently_from_server": "{count} objekt(er) slettet permanent fra Immich-serveren", "assets_downloaded_failed": "{count, plural, one {Nedlasting av # fil - {error} fil feilet} other {Nedlastede # filer - {error} filer feilet}}", - "assets_downloaded_successfully": "{count, plural, one {Downloaded # file successfully} other {Downloaded # files successfully}}", + "assets_downloaded_successfully": "{count, plural, one {Nedlastet # fil vellykket} other {Nedlastede # filer vellykket}}", "assets_moved_to_trash_count": "Flyttet {count, plural, one {# asset} other {# assets}} til søppel", "assets_permanently_deleted_count": "Permanent slettet {count, plural, one {# asset} other {# assets}}", "assets_removed_count": "Slettet {count, plural, one {# asset} other {# assets}}", @@ -553,6 +573,8 @@ "backup_options_page_title": "Backupinnstillinger", "backup_setting_subtitle": "Administrer opplastingsinnstillinger for bakgrunn og forgrunn", "backward": "Bakover", + "beta_sync": "Beta synkroniseringsstatus", + "beta_sync_subtitle": "HÃĨndter det nye synkroniseringssystemet", "biometric_auth_enabled": "Biometrisk autentisering aktivert", "biometric_locked_out": "Du er lÃĨst ute av biometrisk verifisering", "biometric_no_options": "Ingen biometriske valg tilgjengelige", @@ -570,7 +592,7 @@ "cache_settings_clear_cache_button": "Tøm buffer", "cache_settings_clear_cache_button_title": "Tømmer app-ens buffer. Dette vil ha betydelig innvirkning pÃĨ appens ytelse inntil bufferen er gjenoppbygd.", "cache_settings_duplicated_assets_clear_button": "TØM", - "cache_settings_duplicated_assets_subtitle": "Bilder og videoer som er svartelistet av app'en", + "cache_settings_duplicated_assets_subtitle": "Bilder og videoer som er ignorert av app'en", "cache_settings_duplicated_assets_title": "Dupliserte objekter ({count})", "cache_settings_statistics_album": "Bibliotekminiatyrbilder", "cache_settings_statistics_full": "Originalbilder", @@ -587,6 +609,7 @@ "cancel": "Avbryt", "cancel_search": "Avbryt søk", "canceled": "Avbrutt", + "canceling": "Avbryter", "cannot_merge_people": "Kan ikke slÃĨ sammen personer", "cannot_undo_this_action": "Du kan ikke gjøre om denne handlingen!", "cannot_update_the_description": "Kan ikke oppdatere beskrivelsen", @@ -703,7 +726,7 @@ "daily_title_text_date": "E MMM. dd", "daily_title_text_date_year": "E MMM. dddd, yyyy", "dark": "Mørk", - "darkTheme": "Aktiver mørkt utsende", + "dark_theme": "Aktiver mørk-modus", "date_after": "Dato etter", "date_and_time": "Dato og tid", "date_before": "Dato før", @@ -719,6 +742,7 @@ "default_locale": "Standard sprÃĨkinnstilling", "default_locale_description": "Formater datoer og tall basert pÃĨ nettleserens sprÃĨkinnstilling", "delete": "Slett", + "delete_action_prompt": "{count} permanen slettet", "delete_album": "Slett album", "delete_api_key_prompt": "Er du sikker pÃĨ at du vil slette denne API-nøkkelen?", "delete_dialog_alert": "Disse objektene vil bli slettet permanent fra Immich og fra enheten din", @@ -732,6 +756,7 @@ "delete_key": "Slett nøkkel", "delete_library": "Slett bibliotek", "delete_link": "Slett lenke", + "delete_local_action_prompt": "{count} slettet lokalt", "delete_local_dialog_ok_backed_up_only": "Slett kun sikkerhetskopierte objekter", "delete_local_dialog_ok_force": "Slett uansett", "delete_others": "Slett andre", @@ -745,6 +770,7 @@ "description": "Beskrivelse", "description_input_hint_text": "Legg til beskrivelse ...", "description_input_submit_error": "Feil ved oppdatering av beskrivelse, sjekk loggen for flere detaljer", + "deselect_all": "Avmerk alle", "details": "Detaljer", "direction": "Retning", "disabled": "Deaktivert", @@ -762,6 +788,7 @@ "documentation": "Dokumentasjon", "done": "Ferdig", "download": "Last ned", + "download_action_prompt": "Laster ned {count} objekter", "download_canceled": "Nedlasting avbrutt", "download_complete": "Nedlasting fullført", "download_enqueue": "Nedlasting satt i kø", @@ -799,6 +826,7 @@ "edit_key": "Rediger nøkkel", "edit_link": "Endre lenke", "edit_location": "Endre lokasjon", + "edit_location_action_prompt": "{count} lokasjon endret", "edit_location_dialog_title": "Lokasjon", "edit_name": "Redigere navn", "edit_people": "Rediger personer", @@ -817,6 +845,7 @@ "empty_trash": "Tøm papirkurv", "empty_trash_confirmation": "Er du sikker pÃĨ at du vil tømme søppelbøtta? Dette vil slette alle filene i søppelbøtta permanent fra Immich.\nDu kan ikke angre denne handlingen!", "enable": "Aktivere", + "enable_backup": "Aktiver backup", "enable_biometric_auth_description": "Skriv inn PINkoden for ÃĨ aktivere biometrisk autentisering", "enabled": "Aktivert", "end_date": "Slutt dato", @@ -867,7 +896,7 @@ "incorrect_email_or_password": "Feil epost eller passord", "paths_validation_failed": "{paths, plural, one {# sti} other {# sti}} mislyktes validering", "profile_picture_transparent_pixels": "Profil bilde kan ikke ha gjennomsiktige piksler. Vennligst zoom inn og/eller flytt bilde.", - "quota_higher_than_disk_size": "Du har satt en kvote høyere enn diskstørrelsen", + "quota_higher_than_disk_size": "Du har satt kvoten større enn diskstørrelsen", "unable_to_add_album_users": "Kan ikke legge til brukere i albumet", "unable_to_add_assets_to_shared_link": "Kan ikke legge til bilder til delt lenke", "unable_to_add_comment": "Kan ikke legge til kommentar", @@ -984,6 +1013,7 @@ "failed_to_load_assets": "Feilet med ÃĨ laste fil", "failed_to_load_folder": "Kunne ikke laste inn mappe", "favorite": "Favoritt", + "favorite_action_prompt": "{count} lagt til i favoritter", "favorite_or_unfavorite_photo": "Merk som favoritt eller fjern som favoritt", "favorites": "Favoritter", "favorites_page_no_favorites": "Ingen favorittobjekter funnet", @@ -1022,7 +1052,10 @@ "group_year": "Grupper etter ÃĨr", "haptic_feedback_switch": "Aktivert haptisk tilbakemelding", "haptic_feedback_title": "Haptisk tilbakemelding", - "has_quota": "Har kvote", + "has_quota": "Kvote", + "hash_asset": "Hash objekter", + "hashed_assets": "Hashede objekter", + "hashing": "Hasher", "header_settings_add_header_tip": "Legg til header", "header_settings_field_validator_msg": "Verdi kan ikke vÃĻre null", "header_settings_header_name_input": "Header navn", @@ -1055,6 +1088,7 @@ "host": "Vert", "hour": "Time", "id": "ID", + "idle": "Uvirksom", "ignore_icloud_photos": "Ignorer iCloud bilder", "ignore_icloud_photos_description": "Bilder som er lagret pÃĨ iCloud vil ikke lastes opp til Immich", "image": "Bilde", @@ -1127,6 +1161,7 @@ "library_page_sort_created": "Nylig opplastet", "library_page_sort_last_modified": "Sist endret", "library_page_sort_title": "Albumtittel", + "licenses": "Lisenser", "light": "Lys", "like_deleted": "Som slettede", "link_motion_video": "Koble bevegelsesvideo", @@ -1136,7 +1171,9 @@ "list": "Liste", "loading": "Laster", "loading_search_results_failed": "Klarte ikke ÃĨ laste inn søkeresultater", + "local": "Lokal", "local_asset_cast_failed": "Kan ikke caste et bilde som ikke er lastet opp til serveren", + "local_assets": "Lokale objekter", "local_network": "Lokalt nettverk", "local_network_sheet_info": "Appen vil koble til serveren via denne URL-en nÃĨr du bruker det angitte Wi-Fi-nettverket", "location_permission": "Stedstillatelse", @@ -1173,7 +1210,7 @@ "login_form_save_login": "Forbli innlogget", "login_form_server_empty": "Skriv inn en server-URL.", "login_form_server_error": "Kan ikke koble til server.", - "login_has_been_disabled": "Login har blitt deaktivert.", + "login_has_been_disabled": "Innlogging har blitt deaktivert.", "login_password_changed_error": "Det skjedde en feil ved oppdatering av passordet", "login_password_changed_success": "Passord oppdatert", "logout_all_device_confirmation": "Er du sikker pÃĨ at du vil logge ut av alle enheter?", @@ -1209,7 +1246,7 @@ "map_settings_dark_mode": "Mørk modus", "map_settings_date_range_option_day": "Siste 24 timer", "map_settings_date_range_option_days": "Siste {days} dager", - "map_settings_date_range_option_year": "Sist ÃĨr", + "map_settings_date_range_option_year": "Siste ÃĨr", "map_settings_date_range_option_years": "Siste {years} ÃĨr", "map_settings_dialog_title": "Kartinnstillinger", "map_settings_include_show_archived": "Inkluder arkiverte", @@ -1227,7 +1264,7 @@ "memories_check_back_tomorrow": "Sjekk igjen i morgen for flere minner", "memories_setting_description": "Administrer hva du ser i minnene dine", "memories_start_over": "Start pÃĨ nytt", - "memories_swipe_to_close": "Swipe opp for ÃĨ lukke", + "memories_swipe_to_close": "Sveip opp for ÃĨ lukke", "memory": "Minne", "memory_lane_title": "Minnefelt {title}", "menu": "Meny", @@ -1235,7 +1272,7 @@ "merge_people": "SlÃĨ sammen personer", "merge_people_limit": "Du kan bare slÃĨ sammen opp til 5 fjes om gangen", "merge_people_prompt": "Vil du slÃĨ sammen disse personene? Denne handlingen kan ikke reverseres.", - "merge_people_successfully": "Personene ble vellykket slÃĨtt sammen", + "merge_people_successfully": "SammenslÃĨing av personer var vellykket", "merged_people_count": "SammenslÃĨtt {count, plural, one {# person} other {# people}}", "minimize": "Minimer", "minute": "Minutt", @@ -1246,6 +1283,7 @@ "more": "Mer", "move": "Flytt", "move_off_locked_folder": "Flytt ut av lÃĨst mappe", + "move_to_lock_folder_action_prompt": "{count} lagt til i lÃĨst mappe", "move_to_locked_folder": "Flytt til lÃĨst mappe", "move_to_locked_folder_confirmation": "Disse bildene og videoene vil bli fjernet fra alle albumer, og kun tilgjengelige via den lÃĨste mappen", "moved_to_archive": "Flyttet {count, plural, one {# asset} other {# assets}} til arkivet", @@ -1258,14 +1296,14 @@ "name": "Navn", "name_or_nickname": "Navn eller kallenavn", "networking_settings": "Nettverk", - "networking_subtitle": "Administrer serverendepunktinnstillingene", + "networking_subtitle": "Administrer serverendepunkt-innstillinger", "never": "aldri", "new_album": "Nytt Album", "new_api_key": "Ny API-nøkkel", "new_password": "Nytt passord", "new_person": "Ny person", - "new_pin_code": "Ny PIN kode", - "new_pin_code_subtitle": "Dette er første gang du ÃĨpner den lÃĨste mappen. Lag en PIN kode for ÃĨ sikre tilgangen til denne siden", + "new_pin_code": "Ny PIN-kode", + "new_pin_code_subtitle": "Dette er første gang du ÃĨpner den lÃĨste mappen. Lag en PIN-kode for ÃĨ sikre tilgangen til denne siden", "new_user_created": "Ny bruker opprettet", "new_version_available": "NY VERSJON TILGJENGELIG", "newest_first": "Nyeste først", @@ -1282,7 +1320,7 @@ "no_duplicates_found": "Ingen duplikater ble funnet.", "no_exif_info_available": "Ingen EXIF-informasjon tilgjengelig", "no_explore_results_message": "Last opp flere bilder for ÃĨ utforske samlingen din.", - "no_favorites_message": "Legg til favoritter for ÃĨ raskt finne dine beste bilder og videoer", + "no_favorites_message": "Legg til favoritter for ÃĨ finne dine beste bilder og videoer raskt", "no_libraries_message": "Opprett et eksternt bibliotek for ÃĨ se bildene og videoene dine", "no_locked_photos_message": "Bilder og videoer i den lÃĨste mappen er skjult og vil ikke vises nÃĨr du blar i biblioteket.", "no_name": "Ingen navn", @@ -1292,7 +1330,8 @@ "no_results": "Ingen resultater", "no_results_description": "Prøv et synonym eller mer generelt søkeord", "no_shared_albums_message": "Opprett et album for ÃĨ dele bilder og videoer med personer i nettverket ditt", - "not_in_any_album": "Ikke i noen album", + "no_uploads_in_progress": "Ingen opplasting pÃĨgÃĨr", + "not_in_any_album": "Ikke i noe album", "not_selected": "Ikke valgt", "note_apply_storage_label_to_previously_uploaded assets": "Merk: For ÃĨ bruke lagringsetiketten pÃĨ tidligere opplastede filer, kjør", "notes": "Notater", @@ -1305,7 +1344,7 @@ "notifications": "Notifikasjoner", "notifications_setting_description": "Administrer varsler", "oauth": "OAuth", - "official_immich_resources": "Offisielle Immich Resurser", + "official_immich_resources": "Offisielle Immich-ressurser", "offline": "Frakoblet", "ok": "Ok", "oldest_first": "Eldste først", @@ -1315,7 +1354,7 @@ "onboarding_privacy_description": "Følgene (valgfrie) funksjoner er avhengige av eksterne tjenester, og kan bli deaktivert nÃĨr som helst under innstillinger.", "onboarding_server_welcome_description": "La oss sette opp din instans med noen standard innstillinger.", "onboarding_theme_description": "Velg et fargetema for din bruker. Du kan endre denne senere under dine instillinger.", - "onboarding_user_welcome_description": "La oss fÃĨ deg startet!", + "onboarding_user_welcome_description": "La oss fÃĨ deg i gang!", "onboarding_welcome_user": "Velkommen, {user}", "online": "Tilkoblet", "only_favorites": "Bare favoritter", @@ -1329,6 +1368,7 @@ "original": "original", "other": "Annet", "other_devices": "Andre enheter", + "other_entities": "Andre objekter", "other_variables": "Andre variabler", "owned": "Dine", "owner": "Eier", @@ -1460,6 +1500,7 @@ "purchase_server_description_2": "Støttespiller status", "purchase_server_title": "Server", "purchase_settings_server_activated": "Produktnøkkel for server er administrert av administratoren", + "queue_status": "Kø {count}/{total}", "rating": "Stjernevurdering", "rating_clear": "Slett vurdering", "rating_count": "{count, plural, one {# sjerne} other {# stjerner}}", @@ -1474,7 +1515,7 @@ "recent-albums": "Nylige album", "recent_searches": "Nylige søk", "recently_added": "Nylig lagt til", - "recently_added_page_title": "Nylig lagt til", + "recently_added_page_title": "Nylig oppført", "recently_taken": "Nylig tatt", "recently_taken_page_title": "Nylig Tatt", "refresh": "Oppdater", @@ -1488,6 +1529,8 @@ "refreshing_faces": "Oppdaterer ansikter", "refreshing_metadata": "Oppdaterer matadata", "regenerating_thumbnails": "Regenererer miniatyrbilder", + "remote": "Eksternt", + "remote_assets": "Eksterne objekter", "remove": "Fjern", "remove_assets_album_confirmation": "Er du sikker pÃĨ at du fil slette {count, plural, one {# asset} other {# assets}} fra albumet?", "remove_assets_shared_link_confirmation": "Er du sikker pÃĨ at du vil slette {count, plural, one {# asset} other {# assets}} fra den delte lenken?", @@ -1495,7 +1538,9 @@ "remove_custom_date_range": "Fjern egendefinert datoperiode", "remove_deleted_assets": "Fjern fra frakoblede filer", "remove_from_album": "Fjern fra album", + "remove_from_album_action_prompt": "{count} fjernet fra albumet", "remove_from_favorites": "Fjern fra favoritter", + "remove_from_lock_folder_action_prompt": "{count} fjernet fra lÃĨst mappe", "remove_from_locked_folder": "Fjern fra lÃĨst mappe", "remove_from_locked_folder_confirmation": "Er du sikker pÃĨ at du vil flytte disse bildene og videoene ut av den lÃĨste mappen? De vil bli synlige i biblioteket.", "remove_from_shared_link": "Fjern fra delt lenke", @@ -1523,11 +1568,15 @@ "reset_password": "Tilbakestill passord", "reset_people_visibility": "Tilbakestill personsynlighet", "reset_pin_code": "Resett PINkode", + "reset_sqlite": "Reset SQLite Databasen", + "reset_sqlite_confirmation": "Er du sikker pÃĨ at du vil resette SQLite databasen? Du blir nødt til ÃĨ logge ut og inn igjen for ÃĨ resynkronisere data", + "reset_sqlite_success": "Vellykket resetting av SQLite databasen", "reset_to_default": "Tilbakestill til standard", "resolve_duplicates": "Løs duplikater", "resolved_all_duplicates": "Løste alle duplikater", "restore": "Gjenopprett", "restore_all": "Gjenopprett alle", + "restore_trash_action_prompt": "{count} gjenopprettet fra søppelbøtten", "restore_user": "Gjenopprett bruker", "restored_asset": "Gjenopprettet ressurs", "resume": "Fortsett", @@ -1536,6 +1585,7 @@ "role": "Rolle", "role_editor": "Redigerer", "role_viewer": "Visning", + "running": "Kjører", "save": "Lagre", "save_to_gallery": "Lagre til galleriet", "saved_api_key": "Lagret API-nøkkel", @@ -1630,7 +1680,7 @@ "server_offline": "Server frakoblet", "server_online": "Server tilkoblet", "server_privacy": "Server personvern", - "server_stats": "Server Statistikk", + "server_stats": "Serverstatistikk", "server_version": "Server Versjon", "set": "Sett", "set_as_album_cover": "Sett som albumomslag", @@ -1667,6 +1717,7 @@ "settings_saved": "Innstillinger lagret", "setup_pin_code": "Sett opp en PINkode", "share": "Del", + "share_action_prompt": "Delte {count} objekter", "share_add_photos": "Legg til bilder", "share_assets_selected": "{count} valgt", "share_dialog_preparing": "Forbereder ...", @@ -1768,6 +1819,7 @@ "sort_title": "Tittel", "source": "Kilde", "stack": "Stable", + "stack_action_prompt": "{count} stakket", "stack_duplicates": "Stable duplikater", "stack_select_one_photo": "Velg hovedbilde for bildestabbel", "stack_selected_photos": "Stable valgte bilder", @@ -1787,6 +1839,7 @@ "storage_quota": "Lagringsplass", "storage_usage": "{used} av {available} brukt", "submit": "Send inn", + "success": "Vellykket", "suggestions": "Forslag", "sunrise_on_the_beach": "Soloppgang pÃĨ stranden", "support": "Støtte", @@ -1796,6 +1849,8 @@ "sync": "Synkroniser", "sync_albums": "Synkroniser albumer", "sync_albums_manual_subtitle": "Synkroniser alle opplastede videoer og bilder til det valgte backupalbumet", + "sync_local": "Synkroniser lokalt", + "sync_remote": "Synkroniser eksternt", "sync_upload_album_setting_subtitle": "Opprett og last opp dine bilder og videoer til det valgte albumet pÃĨ Immich", "tag": "Tagg", "tag_assets": "Merk ressurser", @@ -1806,6 +1861,7 @@ "tag_updated": "Oppdater merke: {tag}", "tagged_assets": "Merket {count, plural, one {# asset} other {# assets}}", "tags": "Merker", + "tap_to_run_job": "Trykk for ÃĨ kjøre jobben", "template": "Mal", "theme": "Tema", "theme_selection": "Temavalg", @@ -1838,9 +1894,10 @@ "total": "Total", "total_usage": "Totalt brukt", "trash": "Papirkurv", + "trash_action_prompt": "{count} flyttet til søppel", "trash_all": "Slett alt", "trash_count": "Slett {count, number}", - "trash_delete_asset": "Slett ressurs", + "trash_delete_asset": "Slett objekt", "trash_emptied": "Søppelbøtte tømt", "trash_no_results_message": "Her vises bilder og videoer som er flyttet til papirkurven.", "trash_page_delete_all": "Slett alt", @@ -1852,16 +1909,18 @@ "trash_page_title": "Søppelbøtte ({count})", "trashed_items_will_be_permanently_deleted_after": "Elementer i papirkurven vil bli permanent slettet etter {days, plural, one {# dag} other {# dager}}.", "type": "Type", - "unable_to_change_pin_code": "Klarte ikke ÃĨ endre PINkode", + "unable_to_change_pin_code": "Klarte ikke ÃĨ endre PIN-kode", "unable_to_setup_pin_code": "Klarte ikke ÃĨ sette opp PINkode", "unarchive": "Fjern fra arkiv", + "unarchive_action_prompt": "{count} slettet fra Arkiv", "unarchived_count": "{count, plural, other {uarkivert #}}", "undo": "Angre", "unfavorite": "Fjern favoritt", + "unfavorite_action_prompt": "{count} slettet fra Favoritter", "unhide_person": "Vis person", "unknown": "Ukjent", "unknown_country": "Ukjent Land", - "unknown_year": "Ukjent År", + "unknown_year": "Ukjent ÃĨr", "unlimited": "Ubegrenset", "unlink_motion_video": "Koble fra bevegelsesvideo", "unlink_oauth": "Fjern kobling til OAuth", @@ -1873,14 +1932,17 @@ "unsaved_change": "Ulagrede endringer", "unselect_all": "Fjern alle valg", "unselect_all_duplicates": "Fjern markeringen av alle duplikater", - "unselect_all_in": "Fjern alle i {group}", + "unselect_all_in": "Fjern velging av alle i {group}", "unstack": "avstable", + "unstack_action_prompt": "{count} ustakket", "unstacked_assets_count": "Ikke stablet {count, plural, one {# asset} other {# assets}}", + "untagged": "Umerket", "up_next": "Neste", "updated_at": "Oppdatert", "updated_password": "Passord oppdatert", "upload": "Last opp", "upload_concurrency": "Samtidig opplastning", + "upload_details": "Opplastingsdetaljer", "upload_dialog_info": "Vil du utføre backup av valgte objekt(er) til serveren?", "upload_dialog_title": "Last opp objekt", "upload_errors": "Opplasting fullført med {count, plural, one {# error} other {# errors}}, oppdater siden for ÃĨ se nye opplastingsressurser.", @@ -1912,6 +1974,7 @@ "user_usage_stats_description": "Vis kontobruksstatistikk", "username": "Brukernavn", "users": "Brukere", + "users_added_to_album_count": "Lagt til {count, plural, one {#user} other {#users}} til albumet", "utilities": "Verktøy", "validate": "Valider", "validate_endpoint_error": "Skriv inn en gyldig URL", @@ -1919,7 +1982,7 @@ "version": "Versjon", "version_announcement_closing": "Din venn, Alex", "version_announcement_message": "Hei! En ny versjon av Immich er tilgjengelig. Vennligst ta deg tid til ÃĨ lese utgivelsesnotatene for ÃĨ sikre at oppsettet ditt er oppdatert for ÃĨ forhindre feilkonfigurasjoner, spesielt hvis du bruker WatchTower eller en annen mekanisme som hÃĨndterer oppdatering av Immich-forekomsten din automatisk.", - "version_history": "Verson Historie", + "version_history": "Versjonshistorikk", "version_history_item": "Installert {version} den {date}", "video": "Video", "video_hover_setting": "Spill av forhÃĨndsvisining mens du holder over musepekeren", @@ -1927,9 +1990,10 @@ "videos": "Videoer", "videos_count": "{count, plural, one {# Video} other {# Videoer}}", "view": "Vis", - "view_album": "Vis Album", + "view_album": "Vis album", "view_all": "Vis alle", "view_all_users": "Vis alle brukere", + "view_details": "Vis detaljer", "view_in_timeline": "Vis i tidslinje", "view_link": "Vis lenke", "view_links": "Vis lenker", @@ -1937,7 +2001,7 @@ "view_next_asset": "Vis neste fil", "view_previous_asset": "Vis forrige fil", "view_qr_code": "Vis QR-kode", - "view_stack": "Vis Stabbel", + "view_stack": "Vis stabel", "view_user": "Vis bruker", "viewer_remove_from_stack": "Fjern fra stabling", "viewer_stack_use_as_main_asset": "Bruk som hovedobjekt", @@ -1948,12 +2012,12 @@ "week": "Uke", "welcome": "Velkommen", "welcome_to_immich": "Velkommen til Immich", - "wifi_name": "Wi-Fi Navn", - "wrong_pin_code": "Feil PINkode", + "wifi_name": "Wi-Fi-navn", + "wrong_pin_code": "Feil PIN-kode", "year": "År", "years_ago": "{years, plural, one {# ÃĨr} other {# ÃĨr}} siden", "yes": "Ja", "you_dont_have_any_shared_links": "Du har ingen delte lenker", - "your_wifi_name": "Ditt Wi-Fi navn", + "your_wifi_name": "Ditt Wi-Fi-navn", "zoom_image": "Zoom Bilde" } diff --git a/i18n/nl.json b/i18n/nl.json index 07699e0b31..86cb0fba6e 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -93,7 +93,7 @@ "job_created": "Taak aangemaakt", "job_not_concurrency_safe": "Deze taak kan niet gelijktijdig worden uitgevoerd.", "job_settings": "Achtergrondtaak-instellingen", - "job_settings_description": "Beheer gelijktijdige taken", + "job_settings_description": "Beheer aantal gelijktijdige taken", "job_status": "Taakstatus", "jobs_delayed": "{jobCount, plural, other {# vertraagd}}", "jobs_failed": "{jobCount, plural, other {# mislukt}}", @@ -166,6 +166,20 @@ "metadata_settings_description": "Beheer metadata instellingen", "migration_job": "Migratie", "migration_job_description": "Migreer thumbnails voor assets en gezichten naar de nieuwste mapstructuur", + "nightly_tasks_cluster_faces_setting_description": "Gezichtsherkenning uitvoeren op nieuw gedetecteerde gezichten", + "nightly_tasks_cluster_new_faces_setting": "Cluster nieuwe gezichten", + "nightly_tasks_database_cleanup_setting": "Database opschoon taken", + "nightly_tasks_database_cleanup_setting_description": "Ruim oude data op van de database", + "nightly_tasks_generate_memories_setting": "Genereer herinneringen", + "nightly_tasks_generate_memories_setting_description": "Maak nieuwe herinneringen van assets", + "nightly_tasks_missing_thumbnails_setting": "Genereer ontbrekende thumbnails", + "nightly_tasks_missing_thumbnails_setting_description": "Assets zonder thumbnail in een wachtrij plaatsen voor het genereren van thumbnails", + "nightly_tasks_settings": "Instellingen voor nacht taken", + "nightly_tasks_settings_description": "Beheer nacht taken", + "nightly_tasks_start_time_setting": "Start tijd", + "nightly_tasks_start_time_setting_description": "De tijd waarop de server begint met het uitvoeren van de nacht taken", + "nightly_tasks_sync_quota_usage_setting": "Synchroniseer quota gebruik", + "nightly_tasks_sync_quota_usage_setting_description": "update gebruiker opslag quota, gebaseerd op huidig gebruik", "no_paths_added": "Geen paden toegevoegd", "no_pattern_added": "Geen patroon toegevoegd", "note_apply_storage_label_previous_assets": "Opmerking: om het opslaglabel toe te passen op eerder geÃŧploade assets, voer de volgende taak uit", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "Omleidings-URI voor mobiel", "oauth_mobile_redirect_uri_override": "Omleidings-URI voor mobiele app overschrijven", "oauth_mobile_redirect_uri_override_description": "Inschakelen wanneer de OAuth-provider geen mobiele URI toestaat, zoals ''{callback}''", + "oauth_role_claim": "Rol claim", + "oauth_role_claim_description": "Automatisch admin toegang geven als deze claim aanwezig is. De claim kan 'user' of 'admin' zijn.", "oauth_settings": "OAuth", "oauth_settings_description": "Beheer OAuth inloginstellingen", "oauth_settings_more_details": "Raadpleeg de documentatie voor meer informatie over deze functie.", @@ -305,7 +321,7 @@ "transcoding_policy_description": "Stel in wanneer een video wordt getranscodeerd", "transcoding_preferred_hardware_device": "Voorkeur hardwareapparaat", "transcoding_preferred_hardware_device_description": "Geldt alleen voor VAAPI en QSV. Stelt de dri node in die wordt gebruikt voor hardwaretranscodering.", - "transcoding_preset_preset": "Preset (-preset)", + "transcoding_preset_preset": "Voorkeuze (-preset)", "transcoding_preset_preset_description": "Compressiesnelheid. Langzamere presets produceren kleinere bestanden en verhogen de kwaliteit bij het targeten van een bepaalde bitrate. VP9 negeert snelheden boven 'faster'.", "transcoding_reference_frames": "Referentie frames", "transcoding_reference_frames_description": "Het aantal frames om naar te verwijzen bij het comprimeren van een bepaald frame. Hogere waarden verbeteren de compressie-efficiÃĢntie, maar vertragen de codering. Bij 0 wordt deze waarde automatisch ingesteld.", @@ -357,10 +373,12 @@ "admin_password": "Beheerder wachtwoord", "administration": "Beheer", "advanced": "Geavanceerd", + "advanced_settings_beta_timeline_subtitle": "Probeer de nieuwe app-ervaring", + "advanced_settings_beta_timeline_title": "Beta tijdlijn", "advanced_settings_enable_alternate_media_filter_subtitle": "Gebruik deze optie om media te filteren tijdens de synchronisatie op basis van alternatieve criteria. Gebruik dit enkel als de app problemen heeft met het detecteren van albums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTEEL] Gebruik een alternatieve album synchronisatie filter", "advanced_settings_log_level_title": "Logniveau: {level}", - "advanced_settings_prefer_remote_subtitle": "Sommige apparaten zijn traag met het laden van afbeeldingen die lokaal zijn opgeslagen op het apparaat. Activeer deze instelling om in plaats daarvan externe afbeeldingen te laden.", + "advanced_settings_prefer_remote_subtitle": "Sommige apparaten zijn traag met het laden van lokale afbeeldingen. Activeer deze instelling om in plaats daarvan externe afbeeldingen te laden.", "advanced_settings_prefer_remote_title": "Externe afbeeldingen laden", "advanced_settings_proxy_headers_subtitle": "Definieer proxy headers die Immich bij elk netwerkverzoek moet verzenden", "advanced_settings_proxy_headers_title": "Proxy headers", @@ -388,6 +406,7 @@ "album_options": "Albumopties", "album_remove_user": "Gebruiker verwijderen?", "album_remove_user_confirmation": "Weet je zeker dat je {user} wilt verwijderen?", + "album_search_not_found": "Geen albums gevonden die aan je zoekopdracht voldoen", "album_share_no_users": "Het lijkt erop dat je dit album met alle gebruikers hebt gedeeld, of dat je geen gebruikers hebt om mee te delen.", "album_updated": "Album bijgewerkt", "album_updated_setting_description": "Ontvang een e-mailmelding wanneer een gedeeld album nieuwe assets heeft", @@ -407,6 +426,7 @@ "albums_default_sort_order": "Standaard sorteervolgorde album", "albums_default_sort_order_description": "InitiÃĢle sorteervolgorde bij het maken van nieuwe albums.", "albums_feature_description": "Collectie van assets die je kan delen met andere gebruikers.", + "albums_on_device_count": "Albums op apparaat ({count})", "all": "Alle", "all_albums": "Alle albums", "all_people": "Alle mensen", @@ -427,6 +447,7 @@ "app_settings": "App instellingen", "appears_in": "Komt voor in", "archive": "Archief", + "archive_action_prompt": "{count}toegevoegd aan Archief", "archive_or_unarchive_photo": "Foto archiveren of uit het archief halen", "archive_page_no_archived_assets": "Geen gearchiveerde assets gevonden", "archive_page_title": "Archief ({count})", @@ -459,12 +480,11 @@ "asset_skipped_in_trash": "In prullenbak", "asset_uploaded": "GeÃŧpload", "asset_uploading": "Uploadenâ€Ļ", - "asset_viewer_settings_subtitle": "Beheer je instellingen voor gallerijweergave", - "asset_viewer_settings_title": "Foto weergave", + "asset_viewer_settings_subtitle": "Beheer je instellingen voor galerijweergave", + "asset_viewer_settings_title": "Fotoweergave", "assets": "Assets", "assets_added_count": "{count, plural, one {# asset} other {# assets}} toegevoegd", "assets_added_to_album_count": "{count, plural, one {# asset} other {# assets}} aan het album toegevoegd", - "assets_added_to_name_count": "{count, plural, one {# asset} other {# assets}} toegevoegd aan {hasName, select, true {{name}} other {nieuw album}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {# asset} other {# assets}} konden niet aan album toegevoegd worden", "assets_count": "{count, plural, one {# asset} other {# assets}}", "assets_deleted_permanently": "{count} asset(s) permanent verwijderd", @@ -553,6 +573,8 @@ "backup_options_page_title": "Back-up instellingen", "backup_setting_subtitle": "Beheer achtergrond en voorgrond uploadinstellingen", "backward": "Achteruit", + "beta_sync": "Beta Sync Status", + "beta_sync_subtitle": "Beheer het nieuwe synchronisatiesysteem", "biometric_auth_enabled": "Biometrische authenticatie ingeschakeld", "biometric_locked_out": "Biometrische authenticatie is vergrendeld", "biometric_no_options": "Geen biometrische opties beschikbaar", @@ -570,7 +592,7 @@ "cache_settings_clear_cache_button": "Cache wissen", "cache_settings_clear_cache_button_title": "Wist de cache van de app. Dit zal de presentaties van de app aanzienlijk beïnvloeden totdat de cache opnieuw is opgebouwd.", "cache_settings_duplicated_assets_clear_button": "MAAK VRIJ", - "cache_settings_duplicated_assets_subtitle": "Foto's en video's op de zwarte lijst van de app", + "cache_settings_duplicated_assets_subtitle": "Foto’s en video's die de app negeert", "cache_settings_duplicated_assets_title": "Gedupliceerde assets ({count})", "cache_settings_statistics_album": "Bibliotheekthumbnails", "cache_settings_statistics_full": "Volledige afbeeldingen", @@ -587,6 +609,7 @@ "cancel": "Annuleren", "cancel_search": "Zoeken annuleren", "canceled": "Geannuleerd", + "canceling": "Annuleren", "cannot_merge_people": "Kan mensen niet samenvoegen", "cannot_undo_this_action": "Je kunt deze actie niet ongedaan maken!", "cannot_update_the_description": "Kan de beschrijving niet bijwerken", @@ -697,13 +720,13 @@ "curated_object_page_title": "Dingen", "current_device": "Huidig apparaat", "current_pin_code": "Huidige PIN code", - "current_server_address": "Huidige serveradres", + "current_server_address": "Huidig serveradres", "custom_locale": "Aangepaste landinstelling", "custom_locale_description": "Formatteer datums en getallen op basis van de taal en de regio", "daily_title_text_date": "E dd MMM", "daily_title_text_date_year": "E dd MMM yyyy", "dark": "Donker", - "darkTheme": "Donker thema in-/uitschakelen", + "dark_theme": "Wissel naar donker thema", "date_after": "Datum na", "date_and_time": "Datum en tijd", "date_before": "Datum voor", @@ -719,6 +742,7 @@ "default_locale": "Standaard landinstelling", "default_locale_description": "Formatteer datums en getallen op basis van de landinstellingen van je browser", "delete": "Verwijderen", + "delete_action_prompt": "{count} permanent 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", @@ -732,6 +756,7 @@ "delete_key": "Verwijder key", "delete_library": "Verwijder bibliotheek", "delete_link": "Verwijder link", + "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", @@ -745,6 +770,7 @@ "description": "Beschrijving", "description_input_hint_text": "Beschrijving toevoegen...", "description_input_submit_error": "Beschrijving bijwerken mislukt, controleer het logboek voor meer details", + "deselect_all": "Alles Deselecteren", "details": "Details", "direction": "Richting", "disabled": "Uitgeschakeld", @@ -762,6 +788,7 @@ "documentation": "Documentatie", "done": "Klaar", "download": "Downloaden", + "download_action_prompt": "{count} assets downloaden", "download_canceled": "Download geannuleerd", "download_complete": "Download voltooid", "download_enqueue": "Download in wachtrij", @@ -799,6 +826,7 @@ "edit_key": "Key bewerken", "edit_link": "Link bewerken", "edit_location": "Locatie bewerken", + "edit_location_action_prompt": "{count} locatie(s) aangepast", "edit_location_dialog_title": "Locatie", "edit_name": "Naam bewerken", "edit_people": "Mensen bewerken", @@ -817,6 +845,7 @@ "empty_trash": "Prullenbak leegmaken", "empty_trash_confirmation": "Weet je zeker dat je de prullenbak wilt legen? Hiermee worden alle assets in de prullenbak permanent uit Immich verwijderd.\nJe kunt deze actie niet ongedaan maken!", "enable": "Inschakelen", + "enable_backup": "Back-up aanzetten", "enable_biometric_auth_description": "Voer uw pincode in om biometrische authenticatie in te schakelen", "enabled": "Ingeschakeld", "end_date": "Einddatum", @@ -984,6 +1013,7 @@ "failed_to_load_assets": "Kan assets niet laden", "failed_to_load_folder": "Laden van map mislukt", "favorite": "Favoriet", + "favorite_action_prompt": "{count}toegevoegd aan Favorieten", "favorite_or_unfavorite_photo": "Foto markeren als of verwijderen uit favorieten", "favorites": "Favorieten", "favorites_page_no_favorites": "Geen favoriete assets gevonden", @@ -1023,6 +1053,9 @@ "haptic_feedback_switch": "Aanraaktrillingen inschakelen", "haptic_feedback_title": "Aanraaktrillingen", "has_quota": "Heeft limiet", + "hash_asset": "Hash asset", + "hashed_assets": "Gehashte assets", + "hashing": "Hashen", "header_settings_add_header_tip": "Header toevoegen", "header_settings_field_validator_msg": "Waarde kan niet leeg zijn", "header_settings_header_name_input": "Header naam", @@ -1031,7 +1064,7 @@ "headers_settings_tile_title": "Aangepaste proxy headers", "hi_user": "Hallo {name} ({email})", "hide_all_people": "Verberg alle mensen", - "hide_gallery": "Gallerij verbergen", + "hide_gallery": "Galerij verbergen", "hide_named_person": "Verberg persoon {name}", "hide_password": "Verberg wachtwoord", "hide_person": "Verberg persoon", @@ -1055,6 +1088,7 @@ "host": "Host", "hour": "Uur", "id": "ID", + "idle": "Idle", "ignore_icloud_photos": "Negeer iCloud foto's", "ignore_icloud_photos_description": "Foto's die op iCloud zijn opgeslagen, worden niet geÃŧpload naar de Immich server", "image": "Afbeelding", @@ -1127,6 +1161,7 @@ "library_page_sort_created": "Meest recent gemaakt", "library_page_sort_last_modified": "Laatst aangepast", "library_page_sort_title": "Albumtitel", + "licenses": "Licenties", "light": "Licht", "like_deleted": "Like verwijderd", "link_motion_video": "Koppel bewegende video", @@ -1136,7 +1171,9 @@ "list": "Lijst", "loading": "Laden", "loading_search_results_failed": "Laden van zoekresultaten mislukt", + "local": "Lokaal", "local_asset_cast_failed": "Kan geen asset casten die nog niet geÃŧpload is naar de server", + "local_assets": "Lokale Assets", "local_network": "Lokaal netwerk", "local_network_sheet_info": "De app maakt verbinding met de server via deze URL wanneer het opgegeven WiFi-netwerk wordt gebruikt", "location_permission": "Locatietoestemming", @@ -1246,6 +1283,7 @@ "more": "Meer", "move": "Verplaats", "move_off_locked_folder": "Verplaats uit vergrendelde map", + "move_to_lock_folder_action_prompt": "{count} toegevoegd aan de vergrendelde map", "move_to_locked_folder": "Verplaats naar vergrendelde map", "move_to_locked_folder_confirmation": "Deze foto’s en video’s worden uit alle albums verwijderd en zijn alleen te bekijken in de vergrendelde map", "moved_to_archive": "{count, plural, one {# asset} other {# assets}} verplaatst naar archief", @@ -1292,6 +1330,7 @@ "no_results": "Geen resultaten", "no_results_description": "Probeer een synoniem of een algemener zoekwoord", "no_shared_albums_message": "Maak een album om foto's en video's te delen met mensen in je netwerk", + "no_uploads_in_progress": "Geen uploads bezig", "not_in_any_album": "Niet in een album", "not_selected": "Niet geselecteerd", "note_apply_storage_label_to_previously_uploaded assets": "Opmerking: om het opslaglabel toe te passen op eerder geÃŧploade assets, voer de volgende taak uit", @@ -1329,6 +1368,7 @@ "original": "origineel", "other": "Overige", "other_devices": "Andere apparaten", + "other_entities": "Andere entities", "other_variables": "Andere variabelen", "owned": "Eigenaar", "owner": "Eigenaar", @@ -1460,6 +1500,7 @@ "purchase_server_description_2": "Supporterstatus", "purchase_server_title": "Server", "purchase_settings_server_activated": "De licentiesleutel van de server wordt beheerd door de beheerder", + "queue_status": "Wachtrij {count}/{total}", "rating": "Sterwaardering", "rating_clear": "Waardering verwijderen", "rating_count": "{count, plural, one {# ster} other {# sterren}}", @@ -1488,6 +1529,8 @@ "refreshing_faces": "Gezichten aan het vernieuwen", "refreshing_metadata": "Metadata aan het vernieuwen", "regenerating_thumbnails": "Thumbnails opnieuw aan het genereren", + "remote": "Remote", + "remote_assets": "Remote Assets", "remove": "Verwijderen", "remove_assets_album_confirmation": "Weet je zeker dat je {count, plural, one {# asset} other {# assets}} uit het album wilt verwijderen?", "remove_assets_shared_link_confirmation": "Weet je zeker dat je {count, plural, one {# asset} other {# assets}} uit deze gedeelde link wilt verwijderen?", @@ -1495,7 +1538,9 @@ "remove_custom_date_range": "Aangepast datumbereik verwijderen", "remove_deleted_assets": "Verwijder offline bestanden", "remove_from_album": "Verwijder uit album", + "remove_from_album_action_prompt": "{count} verwijderd uit het album", "remove_from_favorites": "Verwijderen uit favorieten", + "remove_from_lock_folder_action_prompt": "{count} 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", @@ -1523,11 +1568,15 @@ "reset_password": "Wachtwoord resetten", "reset_people_visibility": "Zichtbaarheid mensen resetten", "reset_pin_code": "Reset PIN code", + "reset_sqlite": "Reset SQLite Database", + "reset_sqlite_confirmation": "Ben je zeker dat je de SQLite database wilt resetten? Je zal moetenn uitloggen om de data opnieuw te synchroniseren.", + "reset_sqlite_success": "De SQLite database is succesvol gereset", "reset_to_default": "Resetten naar standaard", "resolve_duplicates": "Duplicaten oplossen", "resolved_all_duplicates": "Alle duplicaten opgelost", "restore": "Herstellen", "restore_all": "Herstel alle", + "restore_trash_action_prompt": "{count} teruggezet uit prullenbak", "restore_user": "Gebruiker herstellen", "restored_asset": "Asset hersteld", "resume": "Hervatten", @@ -1536,6 +1585,7 @@ "role": "Rol", "role_editor": "Bewerker", "role_viewer": "Bekijker", + "running": "Actief", "save": "Opslaan", "save_to_gallery": "Opslaan in galerij", "saved_api_key": "API-sleutel opgeslagen", @@ -1667,6 +1717,7 @@ "settings_saved": "Instellingen opgeslagen", "setup_pin_code": "Stel een PIN code in", "share": "Delen", + "share_action_prompt": "{count} assets gedeeld", "share_add_photos": "Foto's toevoegen", "share_assets_selected": "{count} geselecteerd", "share_dialog_preparing": "Voorbereiden...", @@ -1768,6 +1819,7 @@ "sort_title": "Titel", "source": "Bron", "stack": "Stapel", + "stack_action_prompt": "{count} gestapeld", "stack_duplicates": "Stapel duplicaten", "stack_select_one_photo": "Selecteer ÊÊn primaire foto voor de stapel", "stack_selected_photos": "Geselecteerde foto's stapelen", @@ -1787,6 +1839,7 @@ "storage_quota": "Opslaglimiet", "storage_usage": "{used} van {available} gebruikt", "submit": "Verzenden", + "success": "Succes", "suggestions": "Suggesties", "sunrise_on_the_beach": "Zonsopkomst op het strand", "support": "Ondersteuning", @@ -1796,6 +1849,8 @@ "sync": "Sync", "sync_albums": "Albums synchroniseren", "sync_albums_manual_subtitle": "Synchroniseer alle geÃŧploade video’s en foto’s naar de geselecteerde back-up albums", + "sync_local": "Lokaal synchroniseren", + "sync_remote": "Op afstand synchroniseren", "sync_upload_album_setting_subtitle": "Maak en upload je foto's en video's naar de geselecteerde albums op Immich", "tag": "Tag", "tag_assets": "Assets taggen", @@ -1806,6 +1861,7 @@ "tag_updated": "Tag bijgewerkt: {tag}", "tagged_assets": "{count, plural, one {# asset} other {# assets}} getagd", "tags": "Tags", + "tap_to_run_job": "Klik om job te starten", "template": "Template", "theme": "Thema", "theme_selection": "Thema selectie", @@ -1838,6 +1894,7 @@ "total": "Totaal", "total_usage": "Totaal gebruik", "trash": "Prullenbak", + "trash_action_prompt": "{count} verwijderd naar de prullenbak", "trash_all": "Verplaats alle naar prullenbak", "trash_count": "{count, number} naar prullenbak", "trash_delete_asset": "Assets naar prullenbak verplaatsen of verwijderen", @@ -1855,9 +1912,11 @@ "unable_to_change_pin_code": "PIN code kan niet gewijzigd worden", "unable_to_setup_pin_code": "PIN code kan niet ingesteld worden", "unarchive": "Herstellen uit archief", + "unarchive_action_prompt": "{count} verwijderd uit het archief", "unarchived_count": "{count, plural, other {# verwijderd uit archief}}", "undo": "Ongedaan maken", "unfavorite": "Verwijderen uit favorieten", + "unfavorite_action_prompt": "{count} verwijderd uit favorieten", "unhide_person": "Persoon zichtbaar maken", "unknown": "Onbekend", "unknown_country": "Onbekend Land", @@ -1875,12 +1934,15 @@ "unselect_all_duplicates": "Deselecteer alle duplicaten", "unselect_all_in": "Deselecteer alles in {group}", "unstack": "Ontstapelen", + "unstack_action_prompt": "{count} ontstapeld", "unstacked_assets_count": "{count, plural, one {# asset} other {# assets}} ontstapeld", + "untagged": "Ongemarkeerd", "up_next": "Volgende", "updated_at": "GeÃŧpdatet", "updated_password": "Wachtwoord bijgewerkt", "upload": "Uploaden", - "upload_concurrency": "Upload gelijktijdigheid", + "upload_concurrency": "Aantal gelijktijdige uploads", + "upload_details": "Uploaddetails", "upload_dialog_info": "Wil je een backup maken van de geselecteerde asset(s) op de server?", "upload_dialog_title": "Asset uploaden", "upload_errors": "Upload voltooid met {count, plural, one {# fout} other {# fouten}}, vernieuw de pagina om de nieuwe assets te zien.", @@ -1912,6 +1974,7 @@ "user_usage_stats_description": "Bekijk statistieken van accountgebruik", "username": "Gebruikersnaam", "users": "Gebruikers", + "users_added_to_album_count": "{count, plural, one {# Gebruiker} other {# Gebruikers}} toegevoegd aan album", "utilities": "Gereedschap", "validate": "Valideren", "validate_endpoint_error": "Vul een geldige URL in", @@ -1930,6 +1993,7 @@ "view_album": "Bekijk album", "view_all": "Bekijk alle", "view_all_users": "Bekijk alle gebruikers", + "view_details": "Bekijk details", "view_in_timeline": "Bekijk in tijdlijn", "view_link": "Bekijk link", "view_links": "Links bekijken", diff --git a/i18n/nn.json b/i18n/nn.json index 7fb5fdef02..8b04b5d4b2 100644 --- a/i18n/nn.json +++ b/i18n/nn.json @@ -22,18 +22,20 @@ "add_partner": "Legg til partnar", "add_path": "Legg til sti", "add_photos": "Legg til bilete", + "add_tag": "Legg til tagg", "add_to": "Legg tilâ€Ļ", - "add_to_album": "Legg til album", + "add_to_album": "Legg til i album", "add_to_album_bottom_sheet_added": "Lagt til i {album}", "add_to_album_bottom_sheet_already_exists": "Allereie i {album}", - "add_to_shared_album": "Legg til delt album", + "add_to_shared_album": "Legg til i delt album", "add_url": "Legg til URL", - "added_to_archive": "Lagt til arkiv", - "added_to_favorites": "Lagt til favorittar", - "added_to_favorites_count": "Lagt {count, number} til favorittar", + "added_to_archive": "Lagt til i arkiv", + "added_to_favorites": "Lagt til i favorittar", + "added_to_favorites_count": "La til {count, number} i favorittar", "admin": { "add_exclusion_pattern_description": "Legg til utelatingsmønstre. Du kan bruke jokerteikna *, **, og ? for ÃĨ finne filer som passar mønsteret. For ÃĨ ignorere alle filer i ei mappe kalla \"Raw\", bruk \"Raw\", bruk \"**/Raw/**\". For ÃĨ ignorere alle filer som sluttar pÃĨ \".tif\", bruk \"**/*.tif\". For ÃĨ ignorere ein absolutt sti, bruk \"/path/to/ignore/**\".", - "asset_offline_description": "Denne eksterne bibliotekressursen finst ikkje lenger pÃĨ disk og har blitt flytta til papirkurven. Om fila blei flytta innad i biblioteket, sjekk tidslinja di for den tilsvarande ressursen. For ÃĨ gjenopprette ressursen, vennligst sørg for at filstien under er tilgjengeleg for Immich og skann biblioteket.", + "admin_user": "Admin-brukar", + "asset_offline_description": "Denne eksterne bibliotekressursen finst ikkje lenger pÃĨ disk og har blitt flytta til papirkorga. Om fila blei flytta innad i biblioteket, sjekk tidslinja di for den tilsvarande ressursen. For ÃĨ gjenopprette ressursen, vennligst sørg for at filstien under er tilgjengeleg for Immich og skann biblioteket.", "authentication_settings": "Godkjenningsinnstillingar", "authentication_settings_description": "Handsam passord, OAuth, og godkjenningsinnstillingar", "authentication_settings_disable_all": "Er du sikker at du ynskjer ÃĨ gjera alle innloggingsmetodar uverksame? Innlogging vil bli heilt uverksam.", @@ -43,7 +45,7 @@ "backup_database_enable_description": "Aktiver tryggingskopiering av database", "backup_keep_last_amount": "Antal tryggingskopiar ÃĨ behalde", "backup_settings": "Tryggingskopi-innstillingar", - "backup_settings_description": "Handter innstillingar for tryggingskopiering av database. Merk: Desse jobbane vert ikkje overvaka, og du fÃĨr inga varsling ved feil.", + "backup_settings_description": "Handter innstillingar for tryggingskopiering av database. Merk: Desse jobbane vert ikkje overvaka, og du fÃĨr inga varsling ved feil.", "cleared_jobs": "Rydda jobbar for: {job}", "config_set_by_file": "Oppsettet blir sett av ei oppsettfil", "confirm_delete_library": "Er du sikker at du vil slette biblioteket {library}?", @@ -51,6 +53,7 @@ "confirm_email_below": "For ÃĨ bekrefte, skriv \"{email}\" under", "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?", "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", @@ -66,6 +69,10 @@ "force_delete_user_warning": "ÅTVARING: Handlinga fjernar brukaren og all data. Du kan ikkje angre, og filane kan ikkje gjenopprettast.", "image_format": "Format", "image_format_description": "WebP gjev mindre filstorleik enn JPEG, men er treigare ÃĨ lage.", + "image_fullsize_description": "Bilete i full storleik utan metadata, i bruk nÃĨr zooma inn", + "image_fullsize_enabled": "Skru pÃĨ generering av bilete i full storleik", + "image_fullsize_quality_description": "Kvalitet pÃĨ bilete i full storleik frÃĨ 1-100. Høgare er betre, men gjev større filer.", + "image_fullsize_title": "Innstillingar for bilete i full storleik", "image_prefer_embedded_preview": "Bruk helst innebygd førehandsvisning", "image_prefer_embedded_preview_setting_description": "NÃĨr mogleg bruk innebygd førehandsvisning av RAW bilete som inndata til biletehandsaming. For noko bilete kan det gje meir nøyaktige farger, men kvaliteten kjem an pÃĨ kamera og det kan oppstÃĨ komprimeringsartefakt i bilete.", "image_prefer_wide_gamut": "Bruk helst breitt fargespektrum", @@ -121,6 +128,7 @@ "machine_learning_max_detection_distance": "Maksimal oppdagingsverdi", "machine_learning_max_detection_distance_description": "Den største skilnaden mellom to bilete for ÃĨ rekne dei som duplikat, frÃĨ 0.001-0.1. Større verdiar finn fleire duplikat, men kan gje falske treff.", "machine_learning_max_recognition_distance": "Maksimal attkjenningsverdi", + "machine_learning_max_recognition_distance_description": "Maksimal forskjell pÃĨ to ansikt for ÃĨ bli rekna som same person, pÃĨ ein skala frÃĨ 0-2. Eit lÃĨgare tal kan hindre to personar i ÃĨ bli rekna som den same, medan eit høgare tal kan hindre at same individ vert merka som to forskjellige personar. Merk at det er lettare ÃĨ slÃĨ saman to personar enn ÃĨ dele Êin person i to, sÃĨ sikt mot den lÃĨge sida av skalaen nÃĨr mogleg.", "machine_learning_min_detection_score": "Minimum deteksjonsresultat", "machine_learning_min_detection_score_description": "Minimum tillitspoeng for at eit ansikt skal bli oppdaga, pÃĨ ein skala frÃĨ 0 til 1. LÃĨgare verdiar vil oppdage fleire ansikt, men kan føre til feilaktige treff.", "machine_learning_min_recognized_faces": "Minimum gjenkjende ansikt", @@ -137,16 +145,38 @@ "map_gps_settings_description": "Administrer innstillingar for kart og GPS (Reversert geokoding)", "map_light_style": "Lys modus", "map_settings": "Kart", + "map_settings_description": "Endre kartinnstillingar", + "map_style_description": "URL til eit style.json-karttema", "metadata_extraction_job": "Hent ut metadata", + "metadata_extraction_job_description": "Hent ut metadata frÃĨ kvart bilete, slik som GPS, ansikt og oppløysing", + "metadata_faces_import_setting": "Skru pÃĨ import av ansikt", + "metadata_faces_import_setting_description": "Importer ansikt frÃĨ bilete sine EXIF-data og sidecar-filer", "metadata_settings": "Metadata Innstillinger", + "metadata_settings_description": "Endre metadata-innstillingar", "migration_job": "Migrasjon", "notification_email_from_address": "FrÃĨ adresse", + "notification_email_test_email_failed": "Mislukka sending av test-e-post, sjekk konfigurasjonen din", + "notification_email_test_email_sent": "Det vart sendt ei test-melding til {email}. Sjekk e-posten din.", + "notification_email_username_description": "Brukarnamn for autentisering pÃĨ e-post-serveren", + "notification_enable_email_notifications": "Aktiver e-post-varslingar", "notification_settings": "Varselinnstillingar", + "notification_settings_description": "Endre varslingsinnstillingar, inkludert e-post", "oauth_auto_launch": "Autostart", + "oauth_auto_launch_description": "Start OAuth-innloggingsprosessen automatisk nÃĨr innloggingssida vert opna", + "oauth_auto_register_description": "Registrer nye brukarar automatisk etter innlogging med OAuth", "oauth_button_text": "Tekst pÃĨ knapp", + "oauth_client_secret_description": "Krevjast dersom PKCE (Proof Key for Code Exchange) ikkje støttast av OAuth-tilbydaren", + "oauth_enable_description": "Logg inn med OAuth", + "oauth_settings": "OAuth", + "oauth_settings_description": "Innstillingar for innlogging med OAuth", + "oauth_storage_quota_default": "Standard lagringskvote (GiB)", + "oauth_timeout": "Tidsavbrot pÃĨ førespurnad", + "oauth_timeout_description": "Tidsavbrot for førespurnadar i millisekund", "password_enable_description": "Logg inn med e-post og passord", "password_settings": "Passordinnlogging", + "password_settings_description": "Innstillingar for innlogging med passord", "person_cleanup_job": "Personopprydding", + "quota_size_gib": "Lagringskvote (GiB)", "refreshing_all_libraries": "Laster alle bibliotek opp att", "registration": "Administrator registrering", "registration_description": "Sidan du er den første brukaren pÃĨ systemet, vil du bli utnevnt til administrator og ha ansvar for administrative oppgÃĨver. Du vil Ã˛g opprette eventuelle nye brukarar.", @@ -164,8 +194,17 @@ "server_settings_description": "Administrer serverinnstillingar", "server_welcome_message": "Velkomstmelding", "server_welcome_message_description": "Ei melding som synast pÃĨ innloggingssida.", + "sidecar_job": "Sidecar-metadata", + "sidecar_job_description": "Oppdag eller synkroniser sidecar-metadata frÃĨ filsystemet", + "slideshow_duration_description": "Antal sekund ÃĨ vise kvart bilete", + "storage_template_date_time_sample": "Døme pÃĨ tid {date}", + "storage_template_enable_description": "Aktiver lagringsmal-motoren", + "storage_template_migration": "Overgang til ny lagringsmal", + "storage_template_migration_job": "Omorganisering etter ny lagringsmal", + "storage_template_settings": "Lagringsmal", "system_settings": "Systeminnstillingar", "template_email_preview": "Førehandsvisning", + "thumbnail_generation_job": "Generer miniatyrbilete", "transcoding_acceleration_nvenc": "NVENC (Krev NVIDIA GPU)", "transcoding_acceleration_qsv": "Quick Sync (Krev 7. generasjons Intel CPU eller nyare)", "transcoding_acceleration_rkmpp": "RKMPP (Berre pÃĨ Rockchip SOCer)", @@ -180,7 +219,11 @@ "transcoding_audio_codec": "Lydkodek", "transcoding_audio_codec_description": "Opus er det valet med høgast lydkvalitet, men mindre kompabilitet med gamlare einingar og programvare.", "transcoding_bitrate_description": "Videoar med bitrate over høgste tillatte verdi, eller i eit format som ikkje er tillate", - "transcoding_codecs_learn_more": "For ÃĨ lÃĻre meir om nytta begrep, sjÃĨ FFmpeg dokumentasjon for H.264 codec, HEVC codec and VP9 codec." + "transcoding_codecs_learn_more": "For ÃĨ lÃĻre meir om nytta begrep, sjÃĨ FFmpeg dokumentasjon for H.264 codec, HEVC codec and VP9 codec.", + "transcoding_constant_rate_factor_description": "Videokvalitet. Vanlege verdiar er 23 for H.264, 28 for HEVC, 31 for VP9, og 35 for AV1. LÃĨgare er betre, men gjev større filer.", + "transcoding_hardware_acceleration": "Maskinvare-akselerasjon", + "transcoding_max_bitrate": "Maksimal bitrate", + "transcoding_optimal_description": "Videoar med for høg oppløysing, eller ikkje i eit godkjend format" }, "admin_email": "Adminisrator E-post", "admin_password": "Administratorpassord", diff --git a/i18n/pl.json b/i18n/pl.json index e84c6f53e6..5adc6df943 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -5,7 +5,7 @@ "acknowledge": "Zrozumiałem/łam", "action": "Akcja", "action_common_update": "Aktualizuj", - "actions": "Akcje/i", + "actions": "Akcje", "active": "Aktywne", "activity": "Aktywność", "activity_changed": "Aktywność jest {enabled, select, true {włączona} other {wyłączona}}", @@ -166,6 +166,20 @@ "metadata_settings_description": "Zarządzaj ustawieniami metadanych", "migration_job": "Migracja", "migration_job_description": "Przenieś miniatury zasobÃŗw i twarzy do najnowszej struktury folderÃŗw", + "nightly_tasks_cluster_faces_setting_description": "Uruchom rozpoznawanie twarzy dla nowo wykrytych twarzy", + "nightly_tasks_cluster_new_faces_setting": "Zgrupuj nowe twarze", + "nightly_tasks_database_cleanup_setting": "Zadania związane z czyszczeniem bazy danych", + "nightly_tasks_database_cleanup_setting_description": "Wyczyść stare, nieaktualne dane z bazy danych", + "nightly_tasks_generate_memories_setting": "Generuj wspomnienia", + "nightly_tasks_generate_memories_setting_description": "StwÃŗrz nowe wspomnienia z zasobÃŗw", + "nightly_tasks_missing_thumbnails_setting": "Wygeneruj brakujące miniatury", + "nightly_tasks_missing_thumbnails_setting_description": "Dodaj zasoby bez miniatur do kolejki generowania miniatur", + "nightly_tasks_settings": "Ustawienia nocnych zadań", + "nightly_tasks_settings_description": "Zarządzaj zadaniami wykonywanymi w nocy", + "nightly_tasks_start_time_setting": "Czas rozpoczęcia", + "nightly_tasks_start_time_setting_description": "Czas, w ktÃŗrym serwer rozpoczyna wykonywanie nocnych zadań", + "nightly_tasks_sync_quota_usage_setting": "Zsynchronizuj wykorzystanie kontyngentu", + "nightly_tasks_sync_quota_usage_setting_description": "Zaktualizuj kontyngent przestrzeni dyskowej uÅŧytkownika na podstawie aktualnego zuÅŧycia", "no_paths_added": "Nie dodano ścieÅŧki", "no_pattern_added": "Nie dodano wzoru", "note_apply_storage_label_previous_assets": "Uwaga: aby zastosować etykietę magazynu do wcześniej przesłanych zasobÃŗw, uruchom", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "Mobilny adres zwrotny", "oauth_mobile_redirect_uri_override": "Zapasowy URI przekierowania mobilnego", "oauth_mobile_redirect_uri_override_description": "Włącz, gdy dostawca OAuth nie pozwala na mobilne identyfikatory URI typu ''{callback}''", + "oauth_role_claim": "Oświadczenie roli", + "oauth_role_claim_description": "Automatycznie przyznaj dostęp administratora na podstawie obecności tego oświadczenia. Oświadczenie moÅŧe mieć wartość „uÅŧytkownik” lub „administrator”.", "oauth_settings": "OAuth", "oauth_settings_description": "Zarządzaj ustawieniami logowania OAuth", "oauth_settings_more_details": "Więcej informacji o tej funkcji znajdziesz w dokumentacji.", @@ -357,10 +373,12 @@ "admin_password": "Hasło Administratora", "administration": "Administracja", "advanced": "Zaawansowane", + "advanced_settings_beta_timeline_subtitle": "WyprÃŗbuj nową funkcjonalność aplikacji", + "advanced_settings_beta_timeline_title": "Beta-Timeline", "advanced_settings_enable_alternate_media_filter_subtitle": "UÅŧyj tej opcji do filtrowania mediÃŗw podczas synchronizacji alternatywnych kryteriÃŗw. UÅŧywaj tylko wtedy gdy aplikacja ma problemy z wykrywaniem wszystkich albumÃŗw.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERYMENTALNE] UÅŧyj alternatywnego filtra synchronizacji albumu", "advanced_settings_log_level_title": "Poziom szczegÃŗÅ‚owości dziennika: {level}", - "advanced_settings_prefer_remote_subtitle": "NiektÃŗre urządzenia bardzo wolno ładują miniatury z zasobÃŗw na urządzeniu. Aktywuj to ustawienie, aby ładować zdalne obrazy.", + "advanced_settings_prefer_remote_subtitle": "NiektÃŗre urządzenia bardzo wolno ładują miniatury z lokalnych zasobÃŗw. Aktywuj to ustawienie, aby ładować zdalne obrazy.", "advanced_settings_prefer_remote_title": "Preferuj obrazy zdalne", "advanced_settings_proxy_headers_subtitle": "Zdefiniuj nagÅ‚Ãŗwki proxy, ktÃŗre Immich powinien wysyłać z kaÅŧdym Åŧądaniem sieciowym", "advanced_settings_proxy_headers_title": "NagÅ‚Ãŗwki proxy", @@ -427,6 +445,7 @@ "app_settings": "Ustawienia Aplikacji", "appears_in": "W albumach", "archive": "Archiwum", + "archive_action_prompt": "{count} dodanych do Archiwum", "archive_or_unarchive_photo": "Dodaj lub usuń zasÃŗb z archiwum", "archive_page_no_archived_assets": "Nie znaleziono zarchiwizowanych zasobÃŗw", "archive_page_title": "Archiwum {count}", @@ -464,7 +483,6 @@ "assets": "Zasoby", "assets_added_count": "Dodano {count, plural, one {# zasÃŗb} few {# zasoby} other {# zasobÃŗw}}", "assets_added_to_album_count": "Dodano {count, plural, one {# zasÃŗb} few {# zasoby} other {# zasobÃŗw}} do albumu", - "assets_added_to_name_count": "Dodano {count, plural, one {# zasÃŗb} few {# zasoby} other {# zasobÃŗw}} do {hasName, select, true {{name}} other {new album}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {sztuka Elementu} other {szt. ElementÃŗw}} nie moÅŧe być dodana do albumu", "assets_count": "{count, plural, one {# zasÃŗb} few {# zasoby} other {# zasobÃŗw}}", "assets_deleted_permanently": "{count} zostało trwale usuniętych", @@ -703,7 +721,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Ciemny", - "darkTheme": "Włącz ciemny motyw", + "dark_theme": "Przełącz ciemny motyw", "date_after": "Data po", "date_and_time": "Data i godzina", "date_before": "Data przed", @@ -719,6 +737,7 @@ "default_locale": "Domyślny Region", "default_locale_description": "Formatuj daty i liczby na podstawie ustawień Twojej przeglądarki", "delete": "Usuń", + "delete_action_prompt": "{count} trwale usuniętych", "delete_album": "Usuń album", "delete_api_key_prompt": "Czy na pewno chcesz usunąć ten klucz API?", "delete_dialog_alert": "Te elementy zostaną trwale usunięte z Immich i z Twojego urządzenia", @@ -732,6 +751,7 @@ "delete_key": "Usuń klucz", "delete_library": "Usuń bibliotekę", "delete_link": "Usuń link", + "delete_local_action_prompt": "{count} lokalnie usunięto", "delete_local_dialog_ok_backed_up_only": "Usuń tylko kopię zapasową", "delete_local_dialog_ok_force": "Usuń mimo to", "delete_others": "Usuń inne", @@ -799,6 +819,7 @@ "edit_key": "Edytuj klucz", "edit_link": "Edytuj link", "edit_location": "Edytuj lokalizację", + "edit_location_action_prompt": "{count} edytowana lokalizacja", "edit_location_dialog_title": "Lokalizacja", "edit_name": "Edytuj nazwę", "edit_people": "Edytuj osoby", @@ -984,6 +1005,7 @@ "failed_to_load_assets": "Nie udało się załadować zasobÃŗw", "failed_to_load_folder": "Nie udało się załadować folderu", "favorite": "Ulubione", + "favorite_action_prompt": "{count} dodane do ulubionych", "favorite_or_unfavorite_photo": "Dodaj lub usuń z ulubionych", "favorites": "Ulubione", "favorites_page_no_favorites": "Nie znaleziono ulubionych zasobÃŗw", @@ -1127,6 +1149,7 @@ "library_page_sort_created": "Ostatnio utworzone", "library_page_sort_last_modified": "Ostatnio zmodyfikowany", "library_page_sort_title": "Tytuł albumu", + "licenses": "Licencje", "light": "Jasny", "like_deleted": "Polubienie usunięte", "link_motion_video": "Podłącz ruchome wideo", @@ -1246,6 +1269,7 @@ "more": "Więcej", "move": "Przenieś", "move_off_locked_folder": "Przenieś z folderu zablokowanego", + "move_to_lock_folder_action_prompt": "{count} dodanych do folderu zablokowanego", "move_to_locked_folder": "Przenieś do folderu zablokowanego", "move_to_locked_folder_confirmation": "Te zdjęcia i filmy zostaną usunięte ze wszystkich albumÃŗw i będą widzialne tylko w folderze zablokowanym", "moved_to_archive": "Przeniesiono {count, plural, one {# zasÃŗb} few {# zasoby} other {# zasobÃŗw}} do archiwum", @@ -1495,7 +1519,9 @@ "remove_custom_date_range": "Usuń niestandardowy zakres dat", "remove_deleted_assets": "Usuń Niedostępne Pliki", "remove_from_album": "Usuń z albumu", + "remove_from_album_action_prompt": "{count} usunięto z albumu", "remove_from_favorites": "Usuń z ulubionych", + "remove_from_lock_folder_action_prompt": "{count} usunięte z folderu zablokowanego", "remove_from_locked_folder": "Usuń z folderu zablokowanego", "remove_from_locked_folder_confirmation": "Czy na pewno chcesz przenieść te zdjęcia i filmy z folderu zablokowanego? Będą one widoczne w bibliotece.", "remove_from_shared_link": "Usuń z udostępnionego linku", @@ -1667,6 +1693,7 @@ "settings_saved": "Ustawienia zapisane", "setup_pin_code": "Ustaw kod PIN", "share": "Udostępnij", + "share_action_prompt": "Udostępniono {count} zasobÃŗw", "share_add_photos": "Dodaj zdjęcia", "share_assets_selected": "Wybrano {count}", "share_dialog_preparing": "Przygotowywanieâ€Ļ", @@ -1768,6 +1795,7 @@ "sort_title": "Tytuł", "source": "ÅšrÃŗdło", "stack": "Stos", + "stack_action_prompt": "{count} zgrupowano", "stack_duplicates": "Stos duplikatÃŗw", "stack_select_one_photo": "Wybierz jedno gÅ‚Ãŗwne zdjęcie do stosu", "stack_selected_photos": "Układaj wybrane zdjęcia", @@ -1838,6 +1866,7 @@ "total": "Całkowity", "total_usage": "Całkowite wykorzystanie", "trash": "Kosz", + "trash_action_prompt": "{count} przeniesione do kosza", "trash_all": "Usuń wszystkie", "trash_count": "Kosz {count, number}", "trash_delete_asset": "Kosz/Usuń zasÃŗb", @@ -1855,9 +1884,11 @@ "unable_to_change_pin_code": "Nie moÅŧna zmienić kodu PIN", "unable_to_setup_pin_code": "Nie moÅŧna ustawić kodu PIN", "unarchive": "Cofnij archiwizację", + "unarchive_action_prompt": "{count} usunięto z archiwum", "unarchived_count": "{count, plural, one {# cofnięta archiwizacja} few {# cofnięte archiwizacje} other {# cofniętych archiwizacji}}", "undo": "Cofnij", "unfavorite": "Usuń z ulubionych", + "unfavorite_action_prompt": "{count} usunięto z ulubionych", "unhide_person": "PrzywrÃŗÄ‡ osobę", "unknown": "Nieznany", "unknown_country": "Nieznane państwo", @@ -1875,7 +1906,9 @@ "unselect_all_duplicates": "Odznacz wszystkie duplikaty", "unselect_all_in": "Odznacz wszystkie w {group}", "unstack": "RozÅ‚ÃŗÅŧ stos", + "unstack_action_prompt": "{count} odgrupowano", "unstacked_assets_count": "{count, plural, one {RozłoÅŧony # zasÃŗb} few {RozłoÅŧone # zasoby} other {RozłoÅŧonych # zasobÃŗw}}", + "untagged": "Nieoznaczone", "up_next": "Do następnego", "updated_at": "Zaktualizowany", "updated_password": "Pomyślnie zaktualizowano hasło", @@ -1912,6 +1945,7 @@ "user_usage_stats_description": "Wyświetl statystyki uÅŧytkowania konta", "username": "Nazwa uÅŧytkownika", "users": "UÅŧytkownicy", + "users_added_to_album_count": "Dodano {count, plural, one {# uÅŧytkownika} other {# uÅŧytkownikÃŗw}} do albumu", "utilities": "Narzędzia", "validate": "Walidacja", "validate_endpoint_error": "Proszę wprowadzić prawidłowy adres URL", diff --git a/i18n/pt.json b/i18n/pt.json index bb88cafbfa..1878c9b9cc 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -166,6 +166,20 @@ "metadata_settings_description": "Gerir definiçÃĩes de metadados", "migration_job": "MigraÃ§ÃŖo", "migration_job_description": "Migra miniaturas de ficheiros e rostos para a estrutura de pastas mais recente", + "nightly_tasks_cluster_faces_setting_description": "Executar reconhecimento facial em faces detetadas recentemente", + "nightly_tasks_cluster_new_faces_setting": "Agrupar novas faces", + "nightly_tasks_database_cleanup_setting": "Tarefas de limpeza da base de dados", + "nightly_tasks_database_cleanup_setting_description": "Limpar dados antigos e expirados da base de dados", + "nightly_tasks_generate_memories_setting": "Gerar memÃŗrias", + "nightly_tasks_generate_memories_setting_description": "Criar novas memÃŗrias a partir de ficheiros", + "nightly_tasks_missing_thumbnails_setting": "Gerar miniaturas em falta", + "nightly_tasks_missing_thumbnails_setting_description": "Colocar em fila ficheiros sem miniaturas para a geraÃ§ÃŖo das mesmas", + "nightly_tasks_settings": "DefiniçÃĩes de Tarefas DiÃĄrias", + "nightly_tasks_settings_description": "Gerir tarefas diÃĄrias", + "nightly_tasks_start_time_setting": "Hora de início", + "nightly_tasks_start_time_setting_description": "A hora em qual o servidor começa a executar as tarefas diÃĄrias", + "nightly_tasks_sync_quota_usage_setting": "UtilizaÃ§ÃŖo da quota de sincronizaÃ§ÃŖo", + "nightly_tasks_sync_quota_usage_setting_description": "Atualizar quotas de armazenamento de utilizadores, com base na utilizaÃ§ÃŖo atual", "no_paths_added": "Nenhum caminho adicionado", "no_pattern_added": "Nenhum padrÃŖo adicionado", "note_apply_storage_label_previous_assets": "ObservaÃ§ÃŖo: Para aplicar o RÃŗtulo de Armazenamento a ficheiros carregados anteriormente, execute o", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "URI de redirecionamento mÃŗvel", "oauth_mobile_redirect_uri_override": "SubstituiÃ§ÃŖo de URI de redirecionamento mÃŗvel", "oauth_mobile_redirect_uri_override_description": "Ative quando o provedor do OAuth nÃŖo permite um URI mÃŗvel, como ''{callback}''", + "oauth_role_claim": "ReivindicaÃ§ÃŖo de FunçÃĩes", + "oauth_role_claim_description": "Automaticamente concede acesso de administrador, com base na presença desta reivindicaÃ§ÃŖo. A reivindicaÃ§ÃŖo tanto pode ter \"user\" como \"admin\".", "oauth_settings": "OAuth", "oauth_settings_description": "Gerir definiçÃĩes de inicio de sessÃŖo do OAuth", "oauth_settings_more_details": "Para mais informaçÃĩes sobre esta funcionalidade, veja a documentaÃ§ÃŖo.", @@ -357,10 +373,12 @@ "admin_password": "Palavra-passe do administrador", "administration": "AdministraÃ§ÃŖo", "advanced": "Avançado", + "advanced_settings_beta_timeline_subtitle": "Experimente as novas funcionalidades da aplicaÃ§ÃŖo", + "advanced_settings_beta_timeline_title": "Linha temporal da versÃŖo Beta", "advanced_settings_enable_alternate_media_filter_subtitle": "Utilize esta definiÃ§ÃŖo para filtrar ficheiros durante a sincronizaÃ§ÃŖo baseada em critÊrios alternativos. Utilize apenas se a aplicaÃ§ÃŖo estiver com problemas a detetar todos os ÃĄlbuns.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Utilizar um filtro alternativo de sincronizaÃ§ÃŖo de ÃĄlbuns em dispositivos", "advanced_settings_log_level_title": "Nível de registo: {level}", - "advanced_settings_prefer_remote_subtitle": "Alguns dispositivos sÃŖo extremamente lentos para carregar miniaturas da memÃŗria. Ative esta opÃ§ÃŖo para preferir imagens do servidor.", + "advanced_settings_prefer_remote_subtitle": "Alguns dispositivos sÃŖo extremamente lentos a carregar miniaturas da memÃŗria interna. Ative esta opÃ§ÃŖo para preferir imagens do servidor.", "advanced_settings_prefer_remote_title": "Preferir imagens do servidor", "advanced_settings_proxy_headers_subtitle": "Defina os cabeçalhos do proxy que o Immich deve enviar em todas comunicaçÃĩes com a rede", "advanced_settings_proxy_headers_title": "Cabeçalhos do Proxy", @@ -427,6 +445,7 @@ "app_settings": "DefiniçÃĩes da AplicaÃ§ÃŖo", "appears_in": "Aparece em", "archive": "Arquivo", + "archive_action_prompt": "{count} adicionados ao Arquivo", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", "archive_page_no_archived_assets": "Nenhum arquivo encontrado", "archive_page_title": "Arquivo ({count})", @@ -464,7 +483,6 @@ "assets": "Ficheiros", "assets_added_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}}", "assets_added_to_album_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} ao ÃĄlbum", - "assets_added_to_name_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} a {hasName, select, true {{name}} other {novo ÃĄlbum}}", "assets_cannot_be_added_to_album_count": "NÃŖo foi possível adicionar {count, plural, one {ficheiro} other {ficheiros}} ao ÃĄlbum", "assets_count": "{count, plural, one {# ficheiro} other {# ficheiros}}", "assets_deleted_permanently": "{count} ficheiro(s) eliminado(s) permanentemente", @@ -703,7 +721,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Escuro", - "darkTheme": "Alternar tema escuro", + "dark_theme": "Alternar tema escuro", "date_after": "Data apÃŗs", "date_and_time": "Data e Hora", "date_before": "Data antes", @@ -719,6 +737,7 @@ "default_locale": "LocalizaÃ§ÃŖo PadrÃŖo", "default_locale_description": "Formatar datas e nÃēmeros baseados na linguagem do seu navegador", "delete": "Eliminar", + "delete_action_prompt": "{count} eliminados permanentemente", "delete_album": "Eliminar ÃĄlbum", "delete_api_key_prompt": "Tem a certeza de que deseja eliminar esta chave de API?", "delete_dialog_alert": "Esses arquivos serÃŖo permanentemente apagados do Immich e de seu dispositivo", @@ -732,6 +751,7 @@ "delete_key": "Eliminar chave", "delete_library": "Eliminar Biblioteca", "delete_link": "Eliminar link", + "delete_local_action_prompt": "{count} eliminados localmente", "delete_local_dialog_ok_backed_up_only": "Excluir apenas arquivos com backup", "delete_local_dialog_ok_force": "Excluir mesmo assim", "delete_others": "Excluir outros", @@ -762,6 +782,7 @@ "documentation": "DocumentaÃ§ÃŖo", "done": "Feito", "download": "Transferir", + "download_action_prompt": "A descarregar {count} ficheiros", "download_canceled": "Cancelado", "download_complete": "Sucesso", "download_enqueue": "Na fila", @@ -799,6 +820,7 @@ "edit_key": "Editar chave", "edit_link": "Editar link", "edit_location": "Editar LocalizaÃ§ÃŖo", + "edit_location_action_prompt": "{count} locais alterados", "edit_location_dialog_title": "LocalizaÃ§ÃŖo", "edit_name": "Editar nome", "edit_people": "Editar pessoas", @@ -984,6 +1006,7 @@ "failed_to_load_assets": "Ocorreu um erro ao carregar ficheiros", "failed_to_load_folder": "Ocorreu um erro ao carregar a pasta", "favorite": "Favorito", + "favorite_action_prompt": "{count} adicionados aos favoritos", "favorite_or_unfavorite_photo": "Marcar ou desmarcar a foto como favorita", "favorites": "Favoritos", "favorites_page_no_favorites": "Nenhum favorito encontrado", @@ -1127,6 +1150,7 @@ "library_page_sort_created": "Data de criaÃ§ÃŖo", "library_page_sort_last_modified": "Última modificaÃ§ÃŖo", "library_page_sort_title": "Título do ÃĄlbum", + "licenses": "Licenças", "light": "Claro", "like_deleted": "Gosto removido", "link_motion_video": "Relacionar video animado", @@ -1246,6 +1270,7 @@ "more": "Mais", "move": "Mover", "move_off_locked_folder": "Mover para fora da pasta trancada", + "move_to_lock_folder_action_prompt": "{count} adicionados à pasta trancada", "move_to_locked_folder": "Mover para a pasta trancada", "move_to_locked_folder_confirmation": "Estas fotos e vídeos serÃŖo removidas de todos os ÃĄlbuns, e sÃŗ serÃŖo visíveis na pasta trancada", "moved_to_archive": "{count, plural, one {Foi movido # ficheiro} other {Foram movidos # ficheiros}} para o arquivo", @@ -1495,7 +1520,9 @@ "remove_custom_date_range": "Remover intervalo de datas personalizado", "remove_deleted_assets": "Remover ficheiros indisponíveis", "remove_from_album": "Remover do ÃĄlbum", + "remove_from_album_action_prompt": "{count} removido(s) do ÃĄlbum", "remove_from_favorites": "Remover dos favoritos", + "remove_from_lock_folder_action_prompt": "{count} removidos da pasta trancada", "remove_from_locked_folder": "Remover da pasta trancada", "remove_from_locked_folder_confirmation": "Tem a certeza de que quer mover estas fotos e vídeos para fora da pasta trancada? PassarÃŖo a ser visíveis na biblioteca.", "remove_from_shared_link": "Remover do link partilhado", @@ -1667,6 +1694,7 @@ "settings_saved": "DefiniçÃĩes guardadas", "setup_pin_code": "Configurar um cÃŗdigo PIN", "share": "Partilhar", + "share_action_prompt": "Partilhados {count} ficheiros", "share_add_photos": "Adicionar fotos", "share_assets_selected": "{count} selecionados", "share_dialog_preparing": "Preparando...", @@ -1768,6 +1796,7 @@ "sort_title": "Título", "source": "Fonte", "stack": "Empilhar", + "stack_action_prompt": "{count} empilhados", "stack_duplicates": "Empilhar itens duplicados", "stack_select_one_photo": "Selecione uma foto principal para a pilha", "stack_selected_photos": "Empilhar fotos selecionadas", @@ -1838,6 +1867,7 @@ "total": "Total", "total_usage": "Total utilizado", "trash": "Reciclagem", + "trash_action_prompt": "{count} movidos para a reciclagem", "trash_all": "Mover todos para a reciclagem", "trash_count": "Reciclar {count, number}", "trash_delete_asset": "Eliminar ficheiro", @@ -1855,9 +1885,11 @@ "unable_to_change_pin_code": "NÃŖo foi possível alterar o cÃŗdigo PIN", "unable_to_setup_pin_code": "NÃŖo foi possível configurar o cÃŗdigo PIN", "unarchive": "Desarquivar", + "unarchive_action_prompt": "{count} removidos do Arquivo", "unarchived_count": "{count, plural, other {NÃŖo arquivado #}}", "undo": "Anular", "unfavorite": "Remover favorito", + "unfavorite_action_prompt": "{count} removidos dos Favoritos", "unhide_person": "Exibir pessoa", "unknown": "Desconhecido", "unknown_country": "País desconhecido", @@ -1875,7 +1907,9 @@ "unselect_all_duplicates": "Remover seleÃ§ÃŖo de todos os itens duplicados", "unselect_all_in": "Remover seleÃ§ÃŖo de {group}", "unstack": "Desempilhar", + "unstack_action_prompt": "{count} desempilhados", "unstacked_assets_count": "Desempilhados {count, plural, one {# ficheiro} other {# ficheiros}}", + "untagged": "Marcador removido", "up_next": "A seguir", "updated_at": "Atualizado a", "updated_password": "Palavra-passe atualizada", @@ -1912,6 +1946,7 @@ "user_usage_stats_description": "Ver estatísticas de utilizaÃ§ÃŖo de conta", "username": "Nome de utilizador", "users": "Utilizadores", + "users_added_to_album_count": "{count, plural, one {Foi adicionado # utilizador} other {Foram adicionados # utilizadores}} ao ÃĄlbum", "utilities": "Ferramentas", "validate": "Validar", "validate_endpoint_error": "Digite uma URL vÃĄlida", @@ -1951,7 +1986,7 @@ "wifi_name": "Nome da rede Wi-Fi", "wrong_pin_code": "CÃŗdigo PIN errado", "year": "Ano", - "years_ago": "HÃĄ {years, plural, one {# ano} other {# anos}} atrÃĄs", + "years_ago": "HÃĄ {years, plural, one {# ano} other {# anos}}", "yes": "Sim", "you_dont_have_any_shared_links": "NÃŖo tem links partilhados", "your_wifi_name": "Nome da sua rede Wi-Fi", diff --git a/i18n/pt_BR.json b/i18n/pt_BR.json index 705180cafd..1af3bda2ac 100644 --- a/i18n/pt_BR.json +++ b/i18n/pt_BR.json @@ -105,7 +105,7 @@ "library_scanning_enable_description": "Habilitar verificaÃ§ÃŖo periÃŗdica da biblioteca", "library_settings": "Biblioteca Externa", "library_settings_description": "Gerenciar configuraçÃĩes de biblioteca externa", - "library_tasks_description": "Escanear bibliotecas externas para ativos novos ou modificados", + "library_tasks_description": "Verificar se hÃĄ arquivos novos ou modificados nas bibliotecas externas", "library_watching_enable_description": "Observe bibliotecas externas para alteraçÃĩes de arquivos", "library_watching_settings": "ObservaÃ§ÃŖo de biblioteca (EXPERIMENTAL)", "library_watching_settings_description": "Observe automaticamente os arquivos alterados", @@ -168,7 +168,7 @@ "migration_job_description": "Migrar miniaturas de arquivos e rostos para a estrutura de pastas mais recente", "no_paths_added": "Nenhum caminho adicionado", "no_pattern_added": "Nenhum padrÃŖo adicionado", - "note_apply_storage_label_previous_assets": "ObservaÃ§ÃŖo: Para aplicar o rÃŗtulo de armazenamento a arquivos carregados anteriormente, execute o", + "note_apply_storage_label_previous_assets": "ObservaÃ§ÃŖo: Para aplicar o rÃŗtulo de armazenamento a arquivos enviados anteriormente, execute o", "note_cannot_be_changed_later": "NOTA: Isto nÃŖo pode ser alterado posteriormente!", "notification_email_from_address": "E-mail de origem", "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Immich Photo Server \". Tenha certeza de ter permissÃŖo para enviar e-mails a partir do endereço selecionado.", @@ -196,15 +196,17 @@ "oauth_mobile_redirect_uri": "URI de redirecionamento mÃŗvel", "oauth_mobile_redirect_uri_override": "SubstituiÃ§ÃŖo de URI de redirecionamento mÃŗvel", "oauth_mobile_redirect_uri_override_description": "Ative quando o provedor do OAuth nÃŖo suportar uma URI de aplicativo, por exemplo ''{callback}''", + "oauth_role_claim": "DeclaraÃ§ÃŖo de funÃ§ÃŖo", + "oauth_role_claim_description": "DÃĄ permissÃĩes de administrador baseado no valor desta declaraÃ§ÃŖo. A declaraÃ§ÃŖo pode conter os valores 'user' ou 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "Gerenciar configuraçÃĩes de login do OAuth", "oauth_settings_more_details": "Para mais detalhes sobre este recurso, consulte a documentaÃ§ÃŖo.", - "oauth_storage_label_claim": "ReivindicaÃ§ÃŖo de rÃŗtulo de armazenamento", + "oauth_storage_label_claim": "DeclaraÃ§ÃŖo do rÃŗtulo de armazenamento", "oauth_storage_label_claim_description": "Defina automaticamente o rÃŗtulo de armazenamento do usuÃĄrio para o valor desta declaraÃ§ÃŖo.", - "oauth_storage_quota_claim": "Cota de armazenamento", + "oauth_storage_quota_claim": "DeclaraÃ§ÃŖo de cota de armazenamento", "oauth_storage_quota_claim_description": "Defina automaticamente a cota de armazenamento do usuÃĄrio para o valor desta declaraÃ§ÃŖo.", "oauth_storage_quota_default": "Cota de armazenamento padrÃŖo (GiB)", - "oauth_storage_quota_default_description": "Cota em GiB a ser usada quando nenhuma outra reivindicaÃ§ÃŖo for fornecida.", + "oauth_storage_quota_default_description": "Cota em GiB que serÃĄ usada caso esta declaraÃ§ÃŖo nÃŖo seja fornecida.", "oauth_timeout": "Tempo Limite de RequisiÃ§ÃŖo", "oauth_timeout_description": "Tempo limite para requisiçÃĩes, em milissegundos", "password_enable_description": "Login com e-mail e senha", @@ -234,20 +236,20 @@ "sidecar_job_description": "Descubra ou sincronize metadados secundÃĄrios do sistema de arquivos", "slideshow_duration_description": "Tempo em segundos para exibir cada imagem", "smart_search_job_description": "Execute aprendizado de mÃĄquina em arquivos para oferecer suporte à pesquisa inteligente", - "storage_template_date_time_description": "A data e hora da criaÃ§ÃŖo do ativo Ê usado para a informaçÃĩes de data e hora", + "storage_template_date_time_description": "A data e hora da criaÃ§ÃŖo do arquivo Ê usado para a informaçÃĩes de data e hora", "storage_template_date_time_sample": "Exemplo {date}", "storage_template_enable_description": "Habilitar mecanismo de modelo de armazenamento", "storage_template_hash_verification_enabled": "VerificaÃ§ÃŖo de hash ativada", "storage_template_hash_verification_enabled_description": "Ativa a verificaÃ§ÃŖo de hash, nÃŖo desative a menos que vocÃĒ tenha certeza das implicaçÃĩes", "storage_template_migration": "MigraÃ§ÃŖo de modelo de armazenamento", - "storage_template_migration_description": "Aplique o {template} atual aos arquivos carregados anteriormente", - "storage_template_migration_info": "O modelo altera todas extensÃĩes para minÃēsculo. As mudanças no modelo serÃŖo aplicadas apenas em novos arquivos. Para aplicar retroativamente o modelo aos arquivos carregados anteriormente, execute o {job}.", + "storage_template_migration_description": "Aplicar o {template} atual aos arquivos enviados anteriormente", + "storage_template_migration_info": "O modelo altera todas extensÃĩes para minÃēsculo. As mudanças no modelo serÃŖo aplicadas apenas em novos arquivos; para aplicar o modelo aos arquivos enviados anteriormente, execute o {job}.", "storage_template_migration_job": "Tarefa de MigraÃ§ÃŖo de Modelo de Armazenamento", "storage_template_more_details": "Para mais detalhes sobre este recurso, consulte o Modelo de Armazenamento e suas implicaçÃĩes", "storage_template_onboarding_description_v2": "Ao ser ativado, este recurso irÃĄ organizar automaticamente os arquivos com base em um modelo definido pelo usuÃĄrio. Para mais informaçÃĩes, consulte a documentaÃ§ÃŖo.", "storage_template_path_length": "Limite aproximado de comprimento do caminho: {length, number}/{limit, number}", "storage_template_settings": "Modelo de Armazenamento", - "storage_template_settings_description": "Gerencie a estrutura de pasta e o nome do arquivo carregado", + "storage_template_settings_description": "Gerencie a estrutura de pasta e o nome do arquivo enviado", "storage_template_user_label": "{label} Ê o RÃŗtulo de Armazenamento do usuÃĄrio", "system_settings": "ConfiguraçÃĩes do Sistema", "tag_cleanup_job": "Limpeza de marcadores", @@ -336,7 +338,7 @@ "user_delete_delay_settings": "Remover atraso", "user_delete_delay_settings_description": "NÃēmero de dias apÃŗs a remoÃ§ÃŖo para excluir permanentemente a conta e os arquivos de um usuÃĄrio. A tarefa de exclusÃŖo de usuÃĄrio Ê executada à meia-noite para verificar usuÃĄrios que estÃŖo prontos para exclusÃŖo. As alteraçÃĩes nesta configuraÃ§ÃŖo serÃŖo avaliadas na prÃŗxima execuÃ§ÃŖo.", "user_delete_immediately": "A conta e os arquivos de {user} serÃŖo programados para exclusÃŖo permanente imediata.", - "user_delete_immediately_checkbox": "Adicionar o usuÃĄrio e seus ativos na fila para serem deletados imediatamente", + "user_delete_immediately_checkbox": "Adicionar o usuÃĄrio e seus arquivos na fila para serem deletados imediatamente", "user_details": "Detalhes do UsuÃĄrio", "user_management": "Gerenciamento de usuÃĄrios", "user_password_has_been_reset": "A senha do usuÃĄrio foi redefinida:", @@ -426,7 +428,8 @@ "app_bar_signout_dialog_title": "Sair", "app_settings": "ConfiguraçÃĩes do Aplicativo", "appears_in": "Aparece em", - "archive": "Arquivados", + "archive": "Arquivar", + "archive_action_prompt": "{count} mídias arquivadas", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", "archive_page_no_archived_assets": "Nenhum arquivo encontrado", "archive_page_title": "Arquivados ({count})", @@ -440,7 +443,7 @@ "asset_action_share_err_offline": "NÃŖo foi possível obter os arquivos indisponíveis, ignorando", "asset_added_to_album": "Adicionado ao ÃĄlbum", "asset_adding_to_album": "Adicionando ao ÃĄlbumâ€Ļ", - "asset_description_updated": "A descriÃ§ÃŖo do ativo foi atualizada", + "asset_description_updated": "A descriÃ§ÃŖo do arquivo foi atualizada", "asset_filename_is_offline": "O arquivo {filename} nÃŖo estÃĄ disponível", "asset_has_unassigned_faces": "O arquivo tem rostos sem nomes", "asset_hashing": "Processandoâ€Ļ", @@ -457,14 +460,13 @@ "asset_restored_successfully": "Arquivo restaurado", "asset_skipped": "Ignorado", "asset_skipped_in_trash": "Na lixeira", - "asset_uploaded": "Carregado", - "asset_uploading": "Carregandoâ€Ļ", + "asset_uploaded": "Enviado", + "asset_uploading": "Enviandoâ€Ļ", "asset_viewer_settings_subtitle": "Gerenciar as configuraçÃĩes do visualizador da galeria", "asset_viewer_settings_title": "Visualizador de Mídia", "assets": "Arquivos", "assets_added_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}}", "assets_added_to_album_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} ao ÃĄlbum", - "assets_added_to_name_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} {hasName, select, true {ao ÃĄlbum {name}} other {em um novo ÃĄlbum}}", "assets_cannot_be_added_to_album_count": "NÃŖo foi possível adicionar {count, plural, one {o arquivo} other {os arquivos}} ao ÃĄlbum", "assets_count": "{count, plural, one {# arquivo} other {# arquivos}}", "assets_deleted_permanently": "{count} arquivo(s) deletado(s) permanentemente", @@ -489,7 +491,7 @@ "back": "Voltar", "back_close_deselect": "Voltar, fechar ou desmarcar", "background_location_permission": "PermissÃŖo de localizaÃ§ÃŖo em segundo plano", - "background_location_permission_content": "Para que seja possível trocar a URL quando estiver executando em segundo plano, o Immich deve *sempre* ter a permissÃŖo de localizaÃ§ÃŖo precisa para que o aplicativo consiga ler o nome da rede Wi-Fi", + "background_location_permission_content": "Para que seja possível trocar o endereço quando estiver executando em segundo plano, o Immich deve *sempre* ter a permissÃŖo de localizaÃ§ÃŖo precisa para que o aplicativo consiga ler o nome da rede Wi-Fi", "backup_album_selection_page_albums_device": "Álbuns no dispositivo ({count})", "backup_album_selection_page_albums_tap": "Toque para incluir, toque duas vezes para excluir", "backup_album_selection_page_assets_scatter": "Os recursos podem se espalhar por vÃĄrios ÃĄlbuns. Assim, os ÃĄlbuns podem ser incluídos ou excluídos durante o processo de backup.", @@ -502,9 +504,9 @@ "backup_background_service_current_upload_notification": "Enviando {filename}", "backup_background_service_default_notification": "Verificando se hÃĄ novos arquivosâ€Ļ", "backup_background_service_error_title": "Erro no backup", - "backup_background_service_in_progress_notification": "Fazendo backup de seus ativosâ€Ļ", + "backup_background_service_in_progress_notification": "Fazendo backup de seus arquivosâ€Ļ", "backup_background_service_upload_failure_notification": "Falha ao enviar {filename}", - "backup_controller_page_albums": "Álbuns de backup", + "backup_controller_page_albums": "Backup de ÃĄlbuns", "backup_controller_page_background_app_refresh_disabled_content": "Para utilizar o backup em segundo plano, ative a atualizaÃ§ÃŖo da aplicaÃ§ÃŖo em segundo plano em ConfiguraçÃĩes > Geral > AtualizaÃ§ÃŖo em 2Âē plano.", "backup_controller_page_background_app_refresh_disabled_title": "AtualizaÃ§ÃŖo em 2Âē plano desativada", "backup_controller_page_background_app_refresh_enable_button_text": "Ir para as configuraçÃĩes", @@ -512,40 +514,40 @@ "backup_controller_page_background_battery_info_message": "Para uma melhor experiÃĒncia de backup em segundo plano, desative todas as otimizaçÃĩes de bateria que restrinjam a atividade em segundo plano do Immich.\n\nComo isso Ê específico por dispositivo, consulte as informaçÃĩes de como fazer isso com o fabricante do seu dispositivo.", "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "OtimizaçÃĩes de bateria", - "backup_controller_page_background_charging": "Apenas durante o carregamento", + "backup_controller_page_background_charging": "Apenas enquanto carrega a bateria", "backup_controller_page_background_configure_error": "Falha ao configurar o serviço em segundo plano", "backup_controller_page_background_delay": "Adiar backup de novos arquivos: {duration}", - "backup_controller_page_background_description": "Ative o serviço em segundo plano para fazer backup automÃĄtico de novos ativos sem precisar abrir o aplicativo", + "backup_controller_page_background_description": "Ative o serviço em segundo plano para fazer backup automÃĄtico de novos arquivos sem precisar abrir o aplicativo", "backup_controller_page_background_is_off": "O backup automÃĄtico em segundo plano estÃĄ desativado", "backup_controller_page_background_is_on": "O backup automÃĄtico em segundo plano estÃĄ ativado", "backup_controller_page_background_turn_off": "Desativar o serviço em segundo plano", "backup_controller_page_background_turn_on": "Ativar o serviço em segundo plano", "backup_controller_page_background_wifi": "Apenas no Wi-Fi", "backup_controller_page_backup": "Backup", - "backup_controller_page_backup_selected": "Selecionado: ", - "backup_controller_page_backup_sub": "Backup de fotos e vídeos", - "backup_controller_page_created": "Criado em: {date}", - "backup_controller_page_desc_backup": "Ative o backup para carregar automaticamente novos ativos no servidor.", - "backup_controller_page_excluded": "Excluído: ", + "backup_controller_page_backup_selected": "Selecionados: ", + "backup_controller_page_backup_sub": "Total de mídias com backup", + "backup_controller_page_created": "Data: {date}", + "backup_controller_page_desc_backup": "Ative para fazer backup automÃĄtico dos novos arquivos ao abrir este aplicativo.", + "backup_controller_page_excluded": "Ignorados: ", "backup_controller_page_failed": "Falhou ({count})", - "backup_controller_page_filename": "Nome do arquivo: {filename} [{size}]", + "backup_controller_page_filename": "Arquivo: {filename} [{size}]", "backup_controller_page_id": "ID: {id}", - "backup_controller_page_info": "InformaçÃĩes de backup", - "backup_controller_page_none_selected": "Nenhum selecionado", + "backup_controller_page_info": "InformaçÃĩes do backup", + "backup_controller_page_none_selected": "Nenhum ÃĄlbum selecionado", "backup_controller_page_remainder": "Restante", - "backup_controller_page_remainder_sub": "Fotos e vídeos restantes para fazer backup da seleÃ§ÃŖo", + "backup_controller_page_remainder_sub": "Mídias nos ÃĄlbuns selecionados que ainda nÃŖo tem backup", "backup_controller_page_server_storage": "Armazenamento do servidor", - "backup_controller_page_start_backup": "Iniciar backup", - "backup_controller_page_status_off": "O backup estÃĄ desativado", - "backup_controller_page_status_on": "O backup estÃĄ ativado", + "backup_controller_page_start_backup": "Iniciar backup manual", + "backup_controller_page_status_off": "O backup automÃĄtico estÃĄ desativado", + "backup_controller_page_status_on": "O backup automÃĄtico estÃĄ ativado", "backup_controller_page_storage_format": "{used} de {total} usados", - "backup_controller_page_to_backup": "Álbuns para backup", - "backup_controller_page_total_sub": "Todas as fotos e vídeos Ãēnicos dos ÃĄlbuns selecionados", - "backup_controller_page_turn_off": "Desativar o backup", - "backup_controller_page_turn_on": "Ativar Backup", - "backup_controller_page_uploading_file_info": "Carregando informaçÃĩes do arquivo", + "backup_controller_page_to_backup": "Escolha os ÃĄlbuns para fazer backup", + "backup_controller_page_total_sub": "Total de mídias nos ÃĄlbuns selecionados", + "backup_controller_page_turn_off": "Desativar backup automÃĄtico", + "backup_controller_page_turn_on": "Ativar backup automÃĄtico", + "backup_controller_page_uploading_file_info": "InformaçÃĩes do arquivo", "backup_err_only_album": "NÃŖo Ê possível remover o Ãēnico ÃĄlbum", - "backup_info_card_assets": "ativos", + "backup_info_card_assets": "arquivos", "backup_manual_cancelled": "Cancelado", "backup_manual_in_progress": "Envio jÃĄ estÃĄ em progresso. Tente novamente mais tarde", "backup_manual_success": "Sucesso", @@ -570,7 +572,7 @@ "cache_settings_clear_cache_button": "Limpar o cache", "cache_settings_clear_cache_button_title": "Limpa o cache do aplicativo. Isso afetarÃĄ significativamente o desempenho do aplicativo atÊ que o cache seja reconstruído.", "cache_settings_duplicated_assets_clear_button": "LIMPAR", - "cache_settings_duplicated_assets_subtitle": "Fotos e vídeos que sÃŖo bloqueados pelo app", + "cache_settings_duplicated_assets_subtitle": "Mídias ignoradas pelo app", "cache_settings_duplicated_assets_title": "Arquivos duplicados ({count})", "cache_settings_statistics_album": "Miniaturas da biblioteca", "cache_settings_statistics_full": "Imagens completas", @@ -658,9 +660,9 @@ "control_bottom_app_bar_create_new_album": "Criar novo ÃĄlbum", "control_bottom_app_bar_delete_from_immich": "Excluir do Immich", "control_bottom_app_bar_delete_from_local": "Excluir do dispositivo", - "control_bottom_app_bar_edit_location": "Editar LocalizaÃ§ÃŖo", + "control_bottom_app_bar_edit_location": "Alterar Local", "control_bottom_app_bar_edit_time": "Editar data e hora", - "control_bottom_app_bar_share_link": "Compartilhar Link", + "control_bottom_app_bar_share_link": "Link", "control_bottom_app_bar_share_to": "Compartilhar", "control_bottom_app_bar_trash_from_immich": "Mover para a Lixeira", "copied_image_to_clipboard": "Imagem copiada para a ÃĄrea de transferÃĒncia.", @@ -680,7 +682,7 @@ "create_album_page_untitled": "Sem título", "create_library": "Criar biblioteca", "create_link": "Criar link", - "create_link_to_share": "Criar link para partilhar", + "create_link_to_share": "Criar link e compartilhar", "create_link_to_share_description": "Permitir que qualquer pessoa com o link veja a(s) foto(s) selecionada(s)", "create_new": "CRIAR NOVO", "create_new_person": "Criar nova pessoa", @@ -703,7 +705,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Escuro", - "darkTheme": "trocar para tema escuro", + "dark_theme": "Usar tema escuro", "date_after": "Data apÃŗs", "date_and_time": "Data e Hora", "date_before": "Data antes", @@ -719,6 +721,7 @@ "default_locale": "LocalizaÃ§ÃŖo PadrÃŖo", "default_locale_description": "Formatar datas e nÃēmeros baseados na linguagem do seu navegador", "delete": "Excluir", + "delete_action_prompt": "{count} deletados permanentemente", "delete_album": "Excluir ÃĄlbum", "delete_api_key_prompt": "Tem certeza de que deseja excluir esta chave de API?", "delete_dialog_alert": "Esses itens serÃŖo excluídos permanentemente do Immich e do seu dispositivo", @@ -781,7 +784,7 @@ "downloading": "Baixando", "downloading_asset_filename": "Baixando arquivo {filename}", "downloading_media": "Baixando mídia", - "drop_files_to_upload": "Solte arquivos em qualquer lugar para carregar", + "drop_files_to_upload": "Solte os arquivos em qualquer lugar para enviar", "duplicates": "Duplicados", "duplicates_description": "Marque cada grupo indicando quais arquivos, se algum, sÃŖo duplicados", "duration": "DuraÃ§ÃŖo", @@ -799,6 +802,7 @@ "edit_key": "Editar chave", "edit_link": "Editar link", "edit_location": "Editar LocalizaÃ§ÃŖo", + "edit_location_action_prompt": "{count} locais alterados", "edit_location_dialog_title": "LocalizaÃ§ÃŖo", "edit_name": "Editar nome", "edit_people": "Editar pessoas", @@ -814,7 +818,7 @@ "email": "E-mail", "email_notifications": "NotificaçÃĩes por e-mail", "empty_folder": "A pasta estÃĄ vazia", - "empty_trash": "Esvaziar lixo", + "empty_trash": "Esvaziar lixeira", "empty_trash_confirmation": "Tem certeza de que deseja esvaziar a lixeira? Isso removerÃĄ permanentemente do Immich todos os arquivos que estÃŖo na lixeira.\nVocÃĒ nÃŖo pode desfazer esta aÃ§ÃŖo!", "enable": "Habilitar", "enable_biometric_auth_description": "Insira seu cÃŗdigo PIN para ativar a autenticaÃ§ÃŖo por biometria", @@ -855,8 +859,8 @@ "failed_to_edit_shared_link": "Falha ao editar o link compartilhado", "failed_to_get_people": "Falha na obtenÃ§ÃŖo de pessoas", "failed_to_keep_this_delete_others": "Falha ao manter este arquivo e excluir os outros", - "failed_to_load_asset": "NÃŖo foi possível carregar o ativo", - "failed_to_load_assets": "NÃŖo foi possível carregar os ativos", + "failed_to_load_asset": "NÃŖo foi possível carregar o arquivo", + "failed_to_load_assets": "NÃŖo foi possível carregar os arquivos", "failed_to_load_notifications": "Falha ao carregar notificaçÃĩes", "failed_to_load_people": "Falha ao carregar pessoas", "failed_to_remove_product_key": "Falha ao remover a chave do produto", @@ -949,7 +953,7 @@ "unable_to_update_settings": "NÃŖo foi possível atualizar as configuraçÃĩes", "unable_to_update_timeline_display_status": "NÃŖo foi possível atualizar o modo de visualizaÃ§ÃŖo da linha do tempo", "unable_to_update_user": "NÃŖo foi possível atualizar o usuÃĄrio", - "unable_to_upload_file": "NÃŖo foi possível carregar o arquivo" + "unable_to_upload_file": "NÃŖo foi possível enviar o arquivo" }, "exif": "Exif", "exif_bottom_sheet_description": "Adicionar descriÃ§ÃŖo...", @@ -977,13 +981,14 @@ "external": "Externo", "external_libraries": "Bibliotecas externas", "external_network": "Rede externa", - "external_network_sheet_info": "Quando nÃŖo estiver na rede Wi-Fi especificada, o aplicativo irÃĄ se conectar usando a primeira URL abaixo que obtiver sucesso, começando do topo da lista para baixo", + "external_network_sheet_info": "Quando nÃŖo estiver na rede Wi-Fi especificada, o aplicativo irÃĄ se conectar usando o primeiro endereço abaixo que obtiver sucesso, começando do topo da lista para baixo", "face_unassigned": "Sem nome", "failed": "Falhou", "failed_to_authenticate": "NÃŖo foi possível autenticar", "failed_to_load_assets": "Falha ao carregar arquivos", "failed_to_load_folder": "Falha ao carregar a pasta", "favorite": "Favorito", + "favorite_action_prompt": "{count} marcados como favorito", "favorite_or_unfavorite_photo": "Marque ou desmarque a foto como favorita", "favorites": "Favoritos", "favorites_page_no_favorites": "Nenhuma mídia favorita encontrada", @@ -1138,7 +1143,7 @@ "loading_search_results_failed": "Falha ao carregar os resultados da pesquisa", "local_asset_cast_failed": "NÃŖo Ê possível transmitir um arquivo que nÃŖo foi enviado ao servidor", "local_network": "Rede local", - "local_network_sheet_info": "O aplicativo irÃĄ se conectar ao servidor atravÊs desta URL quando estiver na rede Wi-Fi especificada", + "local_network_sheet_info": "O aplicativo irÃĄ se conectar ao servidor atravÊs deste endereço quando estiver na rede Wi-Fi especificada", "location_permission": "PermissÃŖo de localizaÃ§ÃŖo", "location_permission_content": "Para utilizar a funÃ§ÃŖo de troca automÃĄtica de URL Ê necessÃĄrio a permissÃŖo de localizaÃ§ÃŖo precisa, para que seja possível ler o nome da rede Wi-Fi", "location_picker_choose_on_map": "Escolha no mapa", @@ -1147,7 +1152,7 @@ "location_picker_longitude_error": "Digite uma longitude vÃĄlida", "location_picker_longitude_hint": "Digite a longitude", "lock": "Trancar", - "locked_folder": "Pasta Trancada", + "locked_folder": "Pasta com senha", "log_out": "Sair", "log_out_all_devices": "Sair de todos dispositivos", "logged_in_as": "UsuÃĄrio atual: {user}", @@ -1246,6 +1251,7 @@ "more": "Mais", "move": "Mover", "move_off_locked_folder": "Mover para fora da pasta com senha", + "move_to_lock_folder_action_prompt": "{count} adicionados à pasta com senha", "move_to_locked_folder": "Mover para a pasta com senha", "move_to_locked_folder_confirmation": "Estas fotos e vídeos serÃŖo removidos de todos os ÃĄlbuns e somente poderÃŖo ser visualizados de dentro da pasta com senha", "moved_to_archive": "{count, plural, one {# mídia foi arquivada} other {# mídias foram arquivadas}}", @@ -1276,12 +1282,12 @@ "no_albums_with_name_yet": "Parece que vocÃĒ ainda nÃŖo tem nenhum ÃĄlbum com esse nome.", "no_albums_yet": "Parece que vocÃĒ ainda nÃŖo tem nenhum ÃĄlbum.", "no_archived_assets_message": "Arquive fotos e vídeos para os ocultar da sua visualizaÃ§ÃŖo de fotos", - "no_assets_message": "CLIQUE PARA CARREGAR SUA PRIMEIRA FOTO", + "no_assets_message": "CLIQUE PARA ENVIAR SUA PRIMEIRA FOTO", "no_assets_to_show": "NÃŖo hÃĄ arquivos para exibir", "no_cast_devices_found": "Nenhum dispositivo encontrado", "no_duplicates_found": "Nenhuma duplicidade foi encontrada.", "no_exif_info_available": "Sem informaçÃĩes exif disponíveis", - "no_explore_results_message": "Carregue mais fotos para explorar sua coleÃ§ÃŖo.", + "no_explore_results_message": "Envie mais fotos para explorar sua coleÃ§ÃŖo.", "no_favorites_message": "Adicione aos favoritos para encontrar suas melhores fotos e vídeos rapidamente", "no_libraries_message": "Crie uma biblioteca externa para ver suas fotos e vídeos", "no_locked_photos_message": "Fotos e vídeos na pasta com senha sÃŖo ocultos e nÃŖo serÃŖo exibidos enquanto explora ou pesquisa na biblioteca.", @@ -1294,7 +1300,7 @@ "no_shared_albums_message": "Crie um ÃĄlbum para compartilhar fotos e vídeos com pessoas em sua rede", "not_in_any_album": "Fora de ÃĄlbum", "not_selected": "NÃŖo selecionado", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rÃŗtulo de armazenamento a arquivos carregados anteriormente, execute o", + "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rÃŗtulo de armazenamento a arquivos enviados anteriormente, execute o", "notes": "Notas", "nothing_here_yet": "Ainda nÃŖo existe nada aqui", "notification_permission_dialog_content": "Para ativar as notificaçÃĩes, vÃĄ em ConfiguraçÃĩes e selecione permitir.", @@ -1369,7 +1375,7 @@ "permanent_deletion_warning_setting_description": "Exibe um aviso ao deletar arquivos de forma permanente", "permanently_delete": "Deletar permanentemente", "permanently_delete_assets_count": "Excluir permanentemente {count, plural, one {asset} other {assets}}", - "permanently_delete_assets_prompt": "VocÃĒ tem certeza de que deseja excluir permanentemente {count, plural, one {este ativo?} other {estes # ativos?}} Esta aÃ§ÃŖo tambÊm removerÃĄ {count, plural, one {o ativo} other {os ativos}} de um ou mais ÃĄlbuns.", + "permanently_delete_assets_prompt": "VocÃĒ tem certeza de que deseja excluir permanentemente {count, plural, one {este arquivo?} other {estes # arquivos?}} Esta aÃ§ÃŖo tambÊm removerÃĄ {count, plural, one {o arquivo} other {os arquivos}} de um ou mais ÃĄlbuns.", "permanently_deleted_asset": "Arquivo deletado permanentemente", "permanently_deleted_assets_count": "{count, plural, one {# arquivo permanentemente excluído} other {# arquivos permanentemente excluídos}}", "permission": "PermissÃŖo", @@ -1495,7 +1501,9 @@ "remove_custom_date_range": "Remover intervalo de datas personalizado", "remove_deleted_assets": "Remover arquivos excluídos", "remove_from_album": "Remover do ÃĄlbum", + "remove_from_album_action_prompt": "{count} removido do ÃĄlbum", "remove_from_favorites": "Remover dos favoritos", + "remove_from_lock_folder_action_prompt": "{count} removidos da pasta com senha", "remove_from_locked_folder": "Remover da pasta com senha", "remove_from_locked_folder_confirmation": "Tem a certeza de que deseja mover estes arquivos para fora da pasta com senha? Eles ficarÃŖo visíveis na biblioteca principal.", "remove_from_shared_link": "Remover do link compartilhado", @@ -1531,7 +1539,7 @@ "restore_user": "Restaurar usuÃĄrio", "restored_asset": "Arquivo restaurado", "resume": "Continuar", - "retry_upload": "Tentar carregar novamente", + "retry_upload": "Tentar enviar novamente", "review_duplicates": "Revisar duplicidade", "role": "FunÃ§ÃŖo", "role_editor": "Editor", @@ -1626,7 +1634,7 @@ "send_welcome_email": "Enviar E-mail de boas vindas", "server_endpoint": "URL do servidor", "server_info_box_app_version": "VersÃŖo do aplicativo", - "server_info_box_server_url": "URL do servidor", + "server_info_box_server_url": "Endereço", "server_offline": "Servidor Indisponível", "server_online": "Servidor Disponível", "server_privacy": "Privacidade do servidor", @@ -1713,7 +1721,7 @@ "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gerenciar links compartilhados", "shared_link_options": "OpçÃĩes de link compartilhado", - "shared_links": "Links compartilhados", + "shared_links": "Links", "shared_links_description": "Compartilhar fotos e videos com um link", "shared_photos_and_videos_count": "{assetCount, plural, one {# Foto & vídeo compartilhado.} other {# Fotos & vídeos compartilhados.}}", "shared_with_me": "Compartilhado comigo", @@ -1838,6 +1846,7 @@ "total": "Total", "total_usage": "UtilizaÃ§ÃŖo total", "trash": "Lixeira", + "trash_action_prompt": "{count} enviados à lixeira", "trash_all": "Mover todos para o lixo", "trash_count": "Lixo {count, number}", "trash_delete_asset": "Jogar na lixeira/Excluir Arquivo", @@ -1855,9 +1864,11 @@ "unable_to_change_pin_code": "NÃŖo foi possível alterar o cÃŗdigo PIN", "unable_to_setup_pin_code": "NÃŖo foi possível criar o cÃŗdigo PIN", "unarchive": "Desarquivar", + "unarchive_action_prompt": "{count} desarquivado", "unarchived_count": "{count, plural, one {# Desarquivado} other {# Desarquivados}}", "undo": "Desfazer", "unfavorite": "Remover favorito", + "unfavorite_action_prompt": "{count} removido dos favoritos", "unhide_person": "Exibir pessoa", "unknown": "Desconhecido", "unknown_country": "País desconhecido", @@ -1876,20 +1887,21 @@ "unselect_all_in": "Remover seleÃ§ÃŖo de {group}", "unstack": "Retirar do grupo", "unstacked_assets_count": "{count, plural, one {# arquivo retirado} other {# arquivos retirados}} do grupo", + "untagged": "Marcador removido", "up_next": "A seguir", "updated_at": "Atualizado em", "updated_password": "Senha atualizada", - "upload": "Carregar", + "upload": "Enviar", "upload_concurrency": "Envios simultÃĸneos", "upload_dialog_info": "Deseja fazer o backup dos arquivos selecionados no servidor?", "upload_dialog_title": "Enviar arquivo", - "upload_errors": "Envio concluído com {count, plural, one {# erro} other {# erros}}, atualize a pÃĄgina para ver os novos arquivos carregados.", + "upload_errors": "Envio concluído com {count, plural, one {# erro} other {# erros}}, atualize a pÃĄgina para ver os novos arquivos.", "upload_progress": "{remaining, number} restantes - {processed, number}/{total, number} jÃĄ processados", "upload_skipped_duplicates": "{count, plural, one {# Arquivo duplicado foi ignorado} other {# Arquivos duplicados foram ignorados}}", "upload_status_duplicates": "Duplicados", "upload_status_errors": "Erros", - "upload_status_uploaded": "Carregado", - "upload_success": "Carregado com sucesso, atualize a pÃĄgina para ver os novos arquivos.", + "upload_status_uploaded": "Enviado", + "upload_success": "Enviado com sucesso, atualize a pÃĄgina para ver os novos arquivos.", "upload_to_immich": "Enviar para o Immich ({count})", "uploading": "Enviando", "url": "URL", diff --git a/i18n/ro.json b/i18n/ro.json index 0b2ef61a36..46197e943f 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -14,6 +14,7 @@ "add_a_location": "Adaugă o locație", "add_a_name": "Adaugă un nume", "add_a_title": "Adaugă un titlu", + "add_endpoint": "Adaugă punct final", "add_exclusion_pattern": "Adăugă un model de excludere", "add_import_path": "Adaugă o cale de import", "add_location": "Adaugă locație", @@ -21,6 +22,7 @@ "add_partner": "Adaugă partener", "add_path": "Adaugă o cale", "add_photos": "Adaugă fotografii", + "add_tag": "Adaugă etichetă", "add_to": "Adaugă laâ€Ļ", "add_to_album": "Adaugă ÃŽn album", "add_to_album_bottom_sheet_added": "Adăugat ÃŽn {album}", @@ -32,6 +34,7 @@ "added_to_favorites_count": "Adăugat {count, number} la favorite", "admin": { "add_exclusion_pattern_description": "Adăugați modele de excludere. Globing folosind *, ** și ? este suportat. Pentru a ignora toate fișierele din orice director numit „Raw”, utilizați „**/Raw/**”. Pentru a ignora toate fișierele care se termină ÃŽn „.tif”, utilizați „**/*.tif”. Pentru a ignora o cale absolută, utilizați „/path/to/ignore/**”.", + "admin_user": "Utilizator admin", "asset_offline_description": "Acest material din biblioteca externă nu se mai găsește pe disc și a fost mutat ÃŽn coșul de gunoi. Dacă fișierul a fost mutat ÃŽn bibliotecă, verificați cronologia pentru noul material corespunzător. Pentru a restabili acest material, asigurați-vă că calea fișierului de mai jos poate fi accesată de Immich și scanați biblioteca.", "authentication_settings": "Setări de Autentificare", "authentication_settings_description": "Gestionează parola, OAuth și alte setări de autentificare", @@ -39,8 +42,8 @@ "authentication_settings_reenable": "Pentru a reactiva, folosește Comandă Server.", "background_task_job": "Activități de Fundal", "backup_database": "Salvare Bază de Date", - "backup_database_enable_description": "Activare salvare bază de date", - "backup_keep_last_amount": "Cantitatea de copii de rezervă anterioare de păstrat", + "backup_database_enable_description": "Activare salvarea bazei de date", + "backup_keep_last_amount": "Număr de copii de rezervă anterioare de păstrat", "backup_settings": "Setări Copii de Rezervă", "backup_settings_description": "Gestionați setările de salvare a bazei de date", "cleared_jobs": "Activități eliminate pentru: {job}", @@ -50,6 +53,7 @@ "confirm_email_below": "Pentru a confirma, tastați „{email}” mai jos", "confirm_reprocess_all_faces": "Sigur doriți să reprocesați toate fețele? Acest lucru va șterge și persoanele cu nume.", "confirm_user_password_reset": "Sigur doriți să resetați parola utilizatorului {user}?", + "confirm_user_pin_code_reset": "Ești sigur că vrei să resetezi codul PIN al {user}?", "create_job": "Creează sarcină", "cron_expression": "Expresia cron", "cron_expression_description": "Setați intervalul de scanare folosind formatul cron. Pentru mai multe informații, consultați de ex. Crontab Guru", @@ -167,7 +171,7 @@ "note_apply_storage_label_previous_assets": "Notă: Pentru a aplica Eticheta de Stocare la elementele ÃŽncărcate anterior, executați", "note_cannot_be_changed_later": "NOTĂ: Nu se va mai putea modifica ulterior!", "notification_email_from_address": "De la adresa", - "notification_email_from_address_description": "Adresa expeditorului, spre exemplu: „Immich Photo Server ”", + "notification_email_from_address_description": "Adresa expeditorului, spre exemplu: „Immich Photo Server ”. Asigură-te că folosești o adresă de la care ai permisiunea de a trimite e-mailuri.", "notification_email_host_description": "Adresa serverului de email (ex. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ingnoră erorile de certificat", "notification_email_ignore_certificate_errors_description": "Ignoră erorile de validare a certificatului TLS (nerecomandat)", @@ -187,6 +191,7 @@ "oauth_auto_register": "Auto ÃŽnregistrare", "oauth_auto_register_description": "Înregistrează automat utilizatori noi după autentificarea cu OAuth", "oauth_button_text": "Text buton", + "oauth_client_secret_description": "Necesar dacă PKCE (Proof Key for Code Exchange) nu este suportat de furnizorul OAuth", "oauth_enable_description": "Autentifică-te cu OAuth", "oauth_mobile_redirect_uri": "URI de redirecționare mobilă", "oauth_mobile_redirect_uri_override": "Înlocuire URI de redirecționare mobilă", @@ -199,7 +204,9 @@ "oauth_storage_quota_claim": "Revendicare spațiu de stocare", "oauth_storage_quota_claim_description": "Setează automat spațiul de stocare al utilizatorului la valoarea acestei cereri.", "oauth_storage_quota_default": "Cota implicită a spațiului de stocare (GiB)", - "oauth_storage_quota_default_description": "Spațiul ÃŽn GiB ce urmează a fi utilizat atunci cÃĸnd nu este furnizată nicio solicitare (introduceți 0 pentru spațiu nelimitat).", + "oauth_storage_quota_default_description": "Spațiul ÃŽn GiB ce urmează a fi utilizat atunci cÃĸnd nu este furnizată nicio solicitare.", + "oauth_timeout": "Solicitarea a expirat", + "oauth_timeout_description": "Timp de expirare pentru solicitări ÃŽn milisecunde", "password_enable_description": "Autentificare cu email și parolĮŽ", "password_settings": "Autentificare cu ParolĮŽ", "password_settings_description": "GestioneazĮŽ setĮŽrile de autentificare cu parola", @@ -237,6 +244,7 @@ "storage_template_migration_info": "Șablonul de stocare va converti extensiile in litere mici. Modificările șablonului se vor aplica doar materialelor noi. Pentru a aplica retroactiv șablonul la materialele ÃŽncărcate anterior, rulați {job}.", "storage_template_migration_job": "Sarcină Migrare Șablon Stocare", "storage_template_more_details": "Pentru mai multe detalii despre aceasta caracteristică, accesați Șablon stocare si implicațiile", + "storage_template_onboarding_description_v2": "CÃĸnd este activată, această funcție va organiza automat fișierele pe baza șablonului definit de către utilizator. Pentru mai multe informații, accesează documentația.", "storage_template_path_length": "Limita de lungime pentru calea aproximativă: {length, number}/{limit, number}", "storage_template_settings": "Șablon Stocare", "storage_template_settings_description": "Gestionează structura folderelor și numele fișierelor pentru elementele ÃŽncărcate", @@ -251,7 +259,7 @@ "template_email_update_album": "Actualizați Șablonul de Album", "template_email_welcome": "Șablon de e-mail de bun venit", "template_settings": "Șabloane de Notificare", - "template_settings_description": "Gestionați șabloanele personalizate pentru notificări.", + "template_settings_description": "Gestionați șabloanele personalizate pentru notificări", "theme_custom_css_settings": "CSS personalizat", "theme_custom_css_settings_description": "Foile de stil ÃŽn cascadă (CSS) permit personalizarea designului Immich.", "theme_settings": "Setări Temă", @@ -283,7 +291,7 @@ "transcoding_encoding_options": "Opțiuni codificare", "transcoding_encoding_options_description": "Setează codecuri , calitatea, rezoluția și alte opțiuni pentru videoclipuri codificare", "transcoding_hardware_acceleration": "Accelerare Hardware", - "transcoding_hardware_acceleration_description": "Experimental; mult mai rapid, dar va avea o calitate mai scăzută la același bitrate", + "transcoding_hardware_acceleration_description": "Experimental: transcodare mai rapidă, dar poate reduce calitatea la aceeași rată de biți", "transcoding_hardware_decoding": "Decodare hardware", "transcoding_hardware_decoding_setting_description": "Se aplică doar pentru NVENC, QSV și RKMPP. Activează accelerarea completă ÃŽn loc de doar accelerarea codificării. S-ar putea să nu funcționeze pentru toate videoclipurile.", "transcoding_max_b_frames": "Număr maxim de cadre B", @@ -329,6 +337,7 @@ "user_delete_delay_settings_description": "Numărul de zile după eliminare pÃĸnă la ștergerea permanentă a contului și a resurselor unui utilizator. Procesul de ștergere a utilizatorului rulează la miezul nopții pentru a verifica utilizatorii care sunt pregătiți pentru ștergere. Modificările aduse acestei setări vor fi evaluate la următoarea execuție.", "user_delete_immediately": "Contul și resursele utilizatorului {user} vor fi puse ÃŽn coadă pentru ștergere permanentă imediat.", "user_delete_immediately_checkbox": "Pune utilizatorul și resursele ÃŽn coadă pentru ștergere imediată", + "user_details": "Detalii utilizator", "user_management": "Gestionarea Utilizatorilor", "user_password_has_been_reset": "Parola utilizatorului a fost resetată:", "user_password_reset_description": "Vă rugăm să furnizați utilizatorului parola temporară și să ÃŽi informați că va trebui să o schimbe la următoarea autentificare.", @@ -353,6 +362,8 @@ "advanced_settings_log_level_title": "Nivel log: {level}", "advanced_settings_prefer_remote_subtitle": "Unele dispozitive ÃŽntÃĸmpină dificultăți ÃŽn ÃŽncărcarea miniaturilor pentru resursele de pe dispozitiv. Activează această setare pentru a ÃŽncărca imaginile de la distanță ÃŽn schimb.", "advanced_settings_prefer_remote_title": "Preferă fotografii la distanță", + "advanced_settings_proxy_headers_subtitle": "Definește antetele proxy pe care Immich ar trebui să le trimită cu fiecare solicitare de rețea", + "advanced_settings_proxy_headers_title": "Antete Proxy", "advanced_settings_self_signed_ssl_subtitle": "Omite verificare certificate SSL pentru distinația server-ului, necesar pentru certificate auto-semnate.", "advanced_settings_self_signed_ssl_title": "Permite certificate SSL auto-semnate", "advanced_settings_sync_remote_deletions_subtitle": "Ștergeți sau restaurați automat un element de pe acest dispozitiv atunci cÃĸnd acțiunea este efectuată pe web", @@ -382,6 +393,7 @@ "album_updated_setting_description": "Primiți o notificare prin e-mail cÃĸnd un album partajat are elemente noi", "album_user_left": "A părăsit {album}", "album_user_removed": "{user} eliminat", + "album_viewer_appbar_delete_confirm": "Ești sigur că vrei să ștergi acest album din contul tău?", "album_viewer_appbar_share_err_delete": "Ștergere album eșuată", "album_viewer_appbar_share_err_leave": "Părăsire album eșuată", "album_viewer_appbar_share_err_remove": "Probleme la ștergerea resurselor din album", @@ -392,6 +404,9 @@ "album_with_link_access": "Permite oricui cu link-ul să vadă fotografiile și persoanele din acest album.", "albums": "Albume", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albume}}", + "albums_default_sort_order": "Ordinea implicită de sortare a albumelor", + "albums_default_sort_order_description": "Ordinea inițială de sortare a pozelor la crearea de albume noi.", + "albums_feature_description": "Colecții de date care pot fi partajate cu alți utilizatori.", "all": "Toate", "all_albums": "Toate albumele", "all_people": "Toți oamenii", @@ -412,11 +427,13 @@ "app_settings": "Setări Aplicație", "appears_in": "Apare ÃŽn", "archive": "Arhivă", + "archive_action_prompt": "{count} adăugate la Arhivă", "archive_or_unarchive_photo": "ArhiveazĮŽ sau dezarhiveazĮŽ fotografia", "archive_page_no_archived_assets": "Nu au fost găsite resurse favorite", "archive_page_title": "Arhivă ({count})", "archive_size": "Mărime arhivă", "archive_size_description": "Configurează dimensiunea arhivei pentru descărcări (ÃŽn GiB)", + "archived": "Arhivat", "archived_count": "{count, plural, other {Arhivat/e#}}", "are_these_the_same_person": "Sunt aceștia aceeași persoană?", "are_you_sure_to_do_this": "Sunteți sigur că doriți să faceți acest lucru?", @@ -427,33 +444,52 @@ "asset_description_updated": "Descrierea resursei a fost actualizată", "asset_filename_is_offline": "Resursa {filename} este offline", "asset_has_unassigned_faces": "Resursa are fețe neatribuite", + "asset_hashing": "Calculare amprentă digitală", + "asset_list_group_by_sub_title": "Grupare după", "asset_list_layout_settings_dynamic_layout_title": "Aspect dinamic", "asset_list_layout_settings_group_automatically": "Automat", "asset_list_layout_settings_group_by": "Grupează resurse după", "asset_list_layout_settings_group_by_month_day": "Lună + zi", + "asset_list_layout_sub_title": "Aspect", "asset_list_settings_subtitle": "Setări format grilă fotografii", "asset_list_settings_title": "Grilă fotografii", "asset_offline": "Resursă Offline", "asset_offline_description": "Această resursă externă nu mai este găsită pe disc. Contactează te rog administratorul tău Immich pentru ajutor.", + "asset_restored_successfully": "Date restaurate cu succes", "asset_skipped": "Sărit", "asset_skipped_in_trash": "În coșul de gunoi", "asset_uploaded": "Încărcat", "asset_uploading": "Se incarcăâ€Ļ", + "asset_viewer_settings_subtitle": "Gestionați setările de vizualizare a galeriei", + "asset_viewer_settings_title": "Vizualizator resurse", "assets": "Resurse", "assets_added_count": "Adăugat {count, plural, one {# resursă} other {# resurse}}", "assets_added_to_album_count": "Am adăugat {count, plural, one {# resursă} other {# resurse}} ÃŽn album", - "assets_added_to_name_count": "Am adăugat {count, plural, one {# resursă} other {# resurse}} ÃŽn {hasName, select, true {{name}} other {albumul nou}}", + "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} nu pot fi adăugate ÃŽn album", "assets_count": "{count, plural, one {# resursă} other {# resurse}}", + "assets_deleted_permanently": "{count} poză/poze ștearsă/șterse permanent", + "assets_deleted_permanently_from_server": "{count} poză/poze ștearsă/șterse permanent din serverul Immich", + "assets_downloaded_failed": "{count, plural, one {S-a descărcat # fișier – {error} fișier eșuat} other {S-au descărcat # fișiere – {error} fișiere eșuate}}", + "assets_downloaded_successfully": "{count, plural, one {S-a descărcat cu succes # fișier} other {S-au descărcat cu succes # fișiere}}", "assets_moved_to_trash_count": "Am mutat {count, plural, one {# resursă} other {# resurse}} ÃŽn coșul de gunoi", "assets_permanently_deleted_count": "Șters permanent {count, plural, one {# resursă} other {# resurse}}", "assets_removed_count": "Eliminat {count, plural, one {# resursă} other {# resurse}}", + "assets_removed_permanently_from_device": "{count} resursă(e) eliminate permanent din dispozitivul dvs.", "assets_restore_confirmation": "Ești sigur că vrei să restaurezi toate resursele tale din coșul de gunoi? Nu poți anula această acțiune! Ține minte că resursele offline nu se restaurează astfel.", "assets_restored_count": "Restaurat {count, plural, one {# resursă} other {# resurse}}", + "assets_restored_successfully": "{count} resursă(e) restaurate cu succes", + "assets_trashed": "{count} resursă(e) eliminate", "assets_trashed_count": "Mutat ÃŽn coșul de gunoi {count, plural, one {# resursă} other {# resurse}}", + "assets_trashed_from_server": "{count} resursă(e) eliminate de pe serverul Immich", "assets_were_part_of_album_count": "{count, plural, one {Resursa era} other {Resursele erau}} deja parte din album", "authorized_devices": "Dispozitive Autorizate", + "automatic_endpoint_switching_subtitle": "Conectează-te local prin rețeaua Wi‐Fi configurată cÃĸnd este valabilă și prin rețele alternative ÃŽn caz contrar", + "automatic_endpoint_switching_title": "Alternare URL automată", + "autoplay_slideshow": "Derulare slideshow automat", "back": "Înapoi", "back_close_deselect": "Înapoi, ÃŽnchidere sau deselectare", + "background_location_permission": "Permisiune locație ÃŽn fundal", + "background_location_permission_content": "Pentru a putea schimba rețeaua activă ÃŽn fundal, Immich are nevoie de acces *permanent* la locația precisă pentru a citi numele rețelei Wi-Fi", "backup_album_selection_page_albums_device": "Albume ÃŽn dispozitiv ({count})", "backup_album_selection_page_albums_tap": "Apasă odata pentru a include, de două ori pentru a exclude", "backup_album_selection_page_assets_scatter": "Resursele pot fi ÃŽmprăștiate ÃŽn mai multe albume. Prin urmare, albumele pot fi incluse sau excluse ÃŽn timpul procesului de backup.", @@ -474,6 +510,7 @@ "backup_controller_page_background_app_refresh_enable_button_text": "Mergi la setări", "backup_controller_page_background_battery_info_link": "Arată-mi cum", "backup_controller_page_background_battery_info_message": "Pentru cea mai bună experiență a backup-ului ÃŽn fundal, te rugăm să dezactivezi orice optimizare pentru baterie care restricționează activitatea ÃŽn fundal pentru Immich.\n\nDeoarece aceasta este specifică fiecărui dispozitiv, te rugăm verifică informațiile necesare tipului tău de dispozitiv.", + "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Optimizări baterie", "backup_controller_page_background_charging": "Doar ÃŽn timpul ÃŽncărcării", "backup_controller_page_background_configure_error": "Configurare serviciu ÃŽn fundal eșuată", @@ -483,7 +520,8 @@ "backup_controller_page_background_is_on": "Backup-ul automat ÃŽn fundal este activat", "backup_controller_page_background_turn_off": "Dezactivează serviciul ÃŽn fundal", "backup_controller_page_background_turn_on": "Activează serviciul ÃŽn fundal", - "backup_controller_page_background_wifi": "Doar conectat la WiFi", + "backup_controller_page_background_wifi": "Numai prin Wi-Fi", + "backup_controller_page_backup": "Backup", "backup_controller_page_backup_selected": "Selectat(e): ", "backup_controller_page_backup_sub": "S-a făcut backup pentru fotografii și videoclipuri", "backup_controller_page_created": "Creat la: {date}", @@ -491,6 +529,7 @@ "backup_controller_page_excluded": "Exclus(e): ", "backup_controller_page_failed": "Eșuate ({count})", "backup_controller_page_filename": "Nume fișier: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informații backup", "backup_controller_page_none_selected": "Nici o selecție", "backup_controller_page_remainder": "Rămas(e)", @@ -504,14 +543,20 @@ "backup_controller_page_total_sub": "Toate fotografiile și videoclipurile unice din albumele selectate", "backup_controller_page_turn_off": "Dezactivează backup-ul ÃŽn prim-plan", "backup_controller_page_turn_on": "Activează backup-ul ÃŽn prim-plan", - "backup_controller_page_uploading_file_info": "Încărcare informații fișier", + "backup_controller_page_uploading_file_info": "Informații ÃŽncărcare fișier", "backup_err_only_album": "Nu poți șterge singurul album", "backup_info_card_assets": "resurse", "backup_manual_cancelled": "Anulat", "backup_manual_in_progress": "Încărcarea este deja ÃŽn curs. Încearcă din nou mai tÃĸrziu", "backup_manual_success": "Succes", "backup_manual_title": "Status ÃŽncărcare", + "backup_options_page_title": "Opțiuni Backup", + "backup_setting_subtitle": "Schimbă opțiuni pentru backup ÃŽn prim-plan și ÃŽn fundal", "backward": "În sens invers", + "biometric_auth_enabled": "Autentificare biometrică activată", + "biometric_locked_out": "Sunteți blocați de la autentificare biometrică", + "biometric_no_options": "Nu sunt disponibile opțiuni biometrice", + "biometric_not_available": "Autentificarea biometrică nu este disponibilă pe acest dispozitiv", "birthdate_saved": "Data nașterii salvată cu succes", "birthdate_set_description": "Data nașterii este utilizată pentru a calcula vÃĸrsta acestei persoane la momentul realizării fotografiei.", "blurred_background": "Fundal neclar", @@ -541,14 +586,19 @@ "camera_model": "Model cameră", "cancel": "Anulați", "cancel_search": "Anulați căutarea", + "canceled": "Anulat", "cannot_merge_people": "Nu se pot ÃŽmbina persoanele", "cannot_undo_this_action": "Nu puteți anula această acțiune!", "cannot_update_the_description": "Nu se poate actualiza descrierea", + "cast": "Partajare", + "cast_description": "Configurați destinațiile de difuzare disponibile", "change_date": "Schimbați data", + "change_description": "Schimbă descrierea", + "change_display_order": "Schimbați ordinea de afișare", "change_expiration_time": "Schimbați data expirare", "change_location": "Schimbați locația", "change_name": "Schimbați nume", - "change_name_successfully": "Schimbare nume cu succes", + "change_name_successfully": "Schimbare a numelui făcută cu succes", "change_password": "Schimbați parolă", "change_password_description": "Aceasta este fie prima dată cÃĸnd te conectezi ÃŽn sistem, fie s-a făcut o solicitare pentru a schimba parola ta. Te rog să introduci noua parolă mai jos.", "change_password_form_confirm_password": "Confirmă parola", @@ -556,8 +606,12 @@ "change_password_form_new_password": "Parolă nouă", "change_password_form_password_mismatch": "Parolele nu se potrivesc", "change_password_form_reenter_new_password": "Reintrodu noua parolă", + "change_pin_code": "Schimbă codul PIN", "change_your_password": "Schimbă-ți parola", "changed_visibility_successfully": "Schimbare vizibilitate cu succes", + "check_corrupt_asset_backup": "Verifică copii de rezervă a resurselor corupte", + "check_corrupt_asset_backup_button": "Efectuează verificarea", + "check_corrupt_asset_backup_description": "Rulează această verificare doar prin Wi-Fi și doar după ce toate resursele au fost salvate ÃŽn copia de rezerva. Procedura poate dura cÃĸteva minute.", "check_logs": "Verificați Jurnale", "choose_matching_people_to_merge": "Alegeți persoanele care se potrivesc pentru a le fuziona", "city": "Oraș", @@ -566,6 +620,14 @@ "clear_all_recent_searches": "Curățați toate căutările recente", "clear_message": "Ștergeți mesajul", "clear_value": "Ștergeți valoarea", + "client_cert_dialog_msg_confirm": "OK", + "client_cert_enter_password": "Introdu Parola", + "client_cert_import": "Importă", + "client_cert_import_success_msg": "Certificatul de client este importat", + "client_cert_invalid_msg": "Fisier cu certificat invalid sau parola este greșită", + "client_cert_remove_msg": "Certificatul de client este șters", + "client_cert_subtitle": "Acceptă doar formatul PKCS12 (.p12, .pfx). Importul/ștergerea certificatului este disponibil(ă) doar ÃŽnainte de autentificare", + "client_cert_title": "Certificat SSL pentru client", "clockwise": "În sensul acelor de ceas", "close": "Închideți", "collapse": "RestrÃĸngeți", @@ -578,19 +640,27 @@ "comments_are_disabled": "Comentariile sunt dezactivate", "common_create_new_album": "Creează album nou", "common_server_error": "Te rugăm să verifici conexiunea la rețea, asigura-te că server-ul este accesibil și că versiunile aplicației/server-ului sunt compatibile.", + "completed": "Finalizat", "confirm": "Confirmați", "confirm_admin_password": "Confirmați Parola de Administrator", "confirm_delete_face": "Ești sigur ca vrei sa ștergi {name} din activ?", "confirm_delete_shared_link": "Sunteți sigur că doriți să ștergeți acest link partajat?", "confirm_keep_this_delete_others": "Toate celelalte active din stivă vor fi șterse, cu excepția acestui material. Sunteți sigur că doriți să continuați?", + "confirm_new_pin_code": "Confirmă noul cod PIN", "confirm_password": "Confirmați parola", + "confirm_tag_face": "Vrei să etichetezi această față ca {name}?", + "confirm_tag_face_unnamed": "Vrei să etichetezi această față?", + "connected_device": "Dispozitiv conectat", + "connected_to": "Conectat la", "contain": "Încadrează", + "context": "Context", "continue": "Continuați", "control_bottom_app_bar_create_new_album": "Creează album nou", "control_bottom_app_bar_delete_from_immich": "Șterge din Immich", "control_bottom_app_bar_delete_from_local": "Șterge din dispozitiv", "control_bottom_app_bar_edit_location": "Editează locație", "control_bottom_app_bar_edit_time": "Editează Data și Ora", + "control_bottom_app_bar_share_link": "Partajează linkul", "control_bottom_app_bar_share_to": "Distribuire către", "control_bottom_app_bar_trash_from_immich": "Mută ÃŽn coș", "copied_image_to_clipboard": "Imagine copiată ÃŽn clipboard.", @@ -612,6 +682,7 @@ "create_link": "Creează link", "create_link_to_share": "Creează link pentru a distribui", "create_link_to_share_description": "Permiteți oricui are link-ul să vadă fotografia (fotografiile) selectată(e)", + "create_new": "CREARE NOUĂ", "create_new_person": "Creați o persoană nouă", "create_new_person_hint": "Atribuiți resursele selectate unei persoane noi", "create_new_user": "Creează utilizator nou", @@ -621,11 +692,16 @@ "create_tag_description": "Creează o etichetă nouă. Pentru etichete imbricate, te rog să introduci calea completă a etichetei, inclusiv bare oblice (/).", "create_user": "Creează utilizator", "created": "Creat", + "created_at": "Creat", + "crop": "Decupează", "curated_object_page_title": "Obiecte", "current_device": "Dispozitiv curent", + "current_pin_code": "Codul PIN actual", + "current_server_address": "Adresa actuală a serverului", "custom_locale": "Setare Regională Personalizată", "custom_locale_description": "Formatați datele și numerele ÃŽn funcție de limbă și regiune", "dark": "Întunecat", + "dark_theme": "Comută tema ÃŽntunecată", "date_after": "După data", "date_and_time": "Dată și oră", "date_before": "Anterior datei", @@ -640,6 +716,7 @@ "default_locale": "Setare Regională Implicită", "default_locale_description": "Formatați datele și numerele ÃŽn funcție de regiunea browserului dvs", "delete": "Ștergere", + "delete_action_prompt": "{count} șterse permanent", "delete_album": "Ștergere album", "delete_api_key_prompt": "Sunteți sigur că doriți să ștergeți această cheie API?", "delete_dialog_alert": "Aceste elemente vor fi șterse permanent de pe server-ul Immich și din dispozitivul tău", @@ -672,6 +749,7 @@ "disallow_edits": "Interzice modificările", "discord": "Server Discord", "discover": "Descoperiți", + "discovered_devices": "Dispozititve descoperite", "dismiss_all_errors": "Ignorați toate erorile", "dismiss_error": "Ignorați eroarea", "display_options": "Opțiuni de afișare", @@ -682,6 +760,12 @@ "documentation": "Documentație", "done": "Gata", "download": "Descărcați", + "download_canceled": "Descărcare anulată", + "download_complete": "Descărcare completă", + "download_enqueue": "Descărcare ÃŽn coadă", + "download_error": "Eroare de descărcare", + "download_failed": "Descărcare eșuată", + "download_finished": "Descărcare finalizată", "download_include_embedded_motion_videos": "Videoclipuri ÃŽncorporate", "download_include_embedded_motion_videos_description": "Include videoclipurile ÃŽncorporate ÃŽn fotografiile ÃŽn mișcare ca fișier separat", "download_settings": "Descărcați", @@ -1204,13 +1288,16 @@ "previous": "Anterior", "previous_memory": "Memoria anterioară", "previous_or_next_photo": "Fotografie ÃŽnainte/ÃŽnapoi", + "previous_or_next_year": "An ÃŽnainte/ÃŽnapoi", "primary": "Primar", "privacy": "Confidențialitate", + "profile": "Profil", "profile_drawer_app_logs": "Log-uri", - "profile_drawer_client_out_of_date_major": "Aplicația nu folosește ultima versiune. Te rugăm să actulizezi la ultima versiune majoră.", - "profile_drawer_client_out_of_date_minor": "Aplicația nu folosește ultima versiune. Te rugăm să actulizezi la ultima versiune minoră.", + "profile_drawer_client_out_of_date_major": "Aplicația nu folosește ultima versiune. Te rugăm să actualizezi la ultima versiune majoră.", + "profile_drawer_client_out_of_date_minor": "Aplicația nu folosește ultima versiune. Te rugăm să actualizezi la ultima versiune minoră.", "profile_drawer_client_server_up_to_date": "Aplicația client și server-ul sunt actualizate", - "profile_drawer_server_out_of_date_major": "Server-ul nu folosește ultima versiune. Te rugăm să actulizezi la ultima versiune majoră.", + "profile_drawer_github": "GitHub", + "profile_drawer_server_out_of_date_major": "Server-ul nu folosește ultima versiune. Te rugăm să actualizezi la ultima versiune majoră.", "profile_drawer_server_out_of_date_minor": "Server-ul nu folosește ultima versiune. Te rugăm să actulizezi la ultima versiune minoră.", "profile_image_of_user": "Imagine de profil a lui {user}", "profile_picture_set": "Poză de profil setată.", @@ -1230,22 +1317,25 @@ "purchase_failed_activation": "Activare eșuată! Vă rugăm să vă verificați e-mailul pentru cheia de produs corectă!", "purchase_individual_description_1": "Pentru un individ", "purchase_individual_description_2": "Statutul de suporter", + "purchase_individual_title": "Individual", "purchase_input_suggestion": "Aveți o cheie de produs? Introduceți cheia mai jos", "purchase_license_subtitle": "Cumpărați Immich pentru a sprijini dezvoltarea continuă a serviciului", "purchase_lifetime_description": "Achiziție pe viață", "purchase_option_title": "OPȚIUNI DE CUMPĂRARE", - "purchase_panel_info_1": "Dezvoltarea Immich necesită mult timp și efort și avem ingineri cu normă ÃŽntreagă care lucrează la ea pentru a o face cÃĸt se poate de bună. Misiunea noastră este ca software-ul open-source și practicile de afaceri etice să devină o sursă de venit durabilă pentru dezvoltatori și să se creeze un ecosistem care să respecte confidențialitatea, cu alternative reale la serviciile cloud care exploatează.", + "purchase_panel_info_1": "Dezvoltarea programului Immich necesită mult timp și efort și avem ingineri cu normă ÃŽntreagă care lucrează la el pentru a-l face cÃĸt se poate de bun. Misiunea noastră este ca software-ul open-source și practicile de afaceri etice să devină o sursă de venit durabilă pentru dezvoltatori și să se creeze un ecosistem care să respecte confidențialitatea utilizatorilor, cu alternative reale la serviciile cloud care exploatează utilizatorii.", "purchase_panel_info_2": "Deoarece ne-am angajat să nu adăugăm planuri de plată, această achiziție nu vă va oferi nicio funcție suplimentară ÃŽn Immich. Ne bazăm pe utilizatori ca dvs. pentru a sprijini dezvoltarea continuă a lui Immich.", "purchase_panel_title": "Susțineți proiectul", + "purchase_per_user": "Per utilizator", "purchase_remove_product_key": "Eliminați Cheia Produsului", "purchase_remove_product_key_prompt": "Sigur doriți să eliminați cheia de produs?", "purchase_remove_server_product_key": "Eliminați cheia de produs a Serverului", "purchase_remove_server_product_key_prompt": "Sigur doriți să eliminați cheia de produs a Serverului?", "purchase_server_description_1": "Pentru tot serverul", "purchase_server_description_2": "Statutul de suporter", + "purchase_server_title": "Server", "purchase_settings_server_activated": "Cheia de produs a serverului este gestionată de administrator", "rating": "Evaluare cu stele", - "rating_clear": "Anulați evaluare", + "rating_clear": "Anulați evaluarea", "rating_count": "{count, plural, one {# stea} other {# stele}}", "rating_description": "Afișați evaluarea EXIF ÃŽn panoul de informații", "reaction_options": "Opțiuni de reacție", @@ -1256,15 +1346,16 @@ "reassing_hint": "Atribuiți resursele selectate unei persoane existente", "recent-albums": "Albume recente", "recent_searches": "Căutări recente", + "recently_added": "Adăugate recent", "recently_added_page_title": "Adăugate recent", "refresh": "ReÃŽmprospătare", - "refresh_encoded_videos": "Actualizează videoclipurile codificate", + "refresh_encoded_videos": "Actualizează videoclipurile encodate", "refresh_faces": "ReÃŽmprospătați fețele", "refresh_metadata": "Actualizați metadatele", "refresh_thumbnails": "ReÃŽmprospătați miniaturile", "refreshed": "ReÃŽmprospătat", "refreshes_every_file": "Recitește toate fișierele existente și noi", - "refreshing_encoded_video": "Se reÃŽmprospătează videoclipul codificat", + "refreshing_encoded_video": "Se reÃŽmprospătează videoclipul encodat", "refreshing_faces": "Se reÃŽmprospătează fețele", "refreshing_metadata": "Se reÃŽmprospătează metadatele", "regenerating_thumbnails": "Se regenerează miniaturile", @@ -1276,9 +1367,12 @@ "remove_deleted_assets": "Eliminați Resursele Șterse", "remove_from_album": "Ștergeți din album", "remove_from_favorites": "Eliminați din favorite", + "remove_from_locked_folder": "Eliminați din folderul securizat", + "remove_from_locked_folder_confirmation": "Sunteți sigur că doriți să mutați aceste poze și videoclipuri afară din folderul securizat? Vor deveni vizibile ÃŽn biblioteca dvs.", "remove_from_shared_link": "Eliminați din linkul partajat", "remove_memory": "Șterge amintirea", "remove_photo_from_memory": "Șterge fotografia din această amintire", + "remove_tag": "Eliminați ticheta", "remove_url": "Eliminați adresa URL", "remove_user": "Eliminați utilizatorul", "removed_api_key": "Cheie API eliminată: {name}", @@ -1647,6 +1741,7 @@ "view_previous_asset": "Vizualizați resursa anterioară", "view_qr_code": "Vezi cod QR", "view_stack": "Vizualizați Stiva", + "view_user": "Vizualizare utilizator", "viewer_remove_from_stack": "Șterge din grup", "viewer_stack_use_as_main_asset": "Folosește ca resursă principală", "viewer_unstack": "Anulează grup", @@ -1656,11 +1751,12 @@ "week": "SĮŽptĮŽmÃĸnĮŽ", "welcome": "Bun venit", "welcome_to_immich": "Bun venit la Immich", - "wifi_name": "WiFi Name", + "wifi_name": "Nume Wi-Fi", + "wrong_pin_code": "Cod PIN greșit", "year": "An", "years_ago": "acum {years, plural, one {# an} other {# ani}} ÃŽn urmă", "yes": "Da", "you_dont_have_any_shared_links": "Nu aveți linkuri partajate", - "your_wifi_name": "Your WiFi name", + "your_wifi_name": "Numele rețelei tale WiFi", "zoom_image": "Măriți Imaginea" } diff --git a/i18n/ru.json b/i18n/ru.json index 5810a31053..c0a01b0be2 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -166,6 +166,20 @@ "metadata_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēаĐŧи ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐŊҋ҅", "migration_job": "ĐœĐ¸ĐŗŅ€Đ°Ņ†Đ¸Ņ", "migration_job_description": "ПĐĩŅ€ĐĩĐŊĐžŅ ĐŧиĐŊĐ¸Đ°Ņ‚ŅŽŅ€ ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ и ĐģĐ¸Ņ† в ĐŋĐžŅĐģĐĩĐ´ĐŊŅŽŅŽ ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€Ņƒ ĐŋаĐŋĐžĐē", + "nightly_tasks_cluster_faces_setting_description": "ЗаĐŋŅƒŅŅ‚Đ¸Ņ‚ŅŒ Ņ€Đ°ŅĐŋОСĐŊаваĐŊиĐĩ ĐģŅŽĐ´ĐĩĐš ĐŋĐž ĐŊĐžĐ˛Ņ‹Đŧ ОйĐŊĐ°Ņ€ŅƒĐļĐĩĐŊĐŊŅ‹Đŧ ĐģĐ¸Ņ†Đ°Đŧ", + "nightly_tasks_cluster_new_faces_setting": "Đ Đ°ŅĐŋОСĐŊаваĐŊиĐĩ ĐŊĐžĐ˛Ņ‹Ņ… ĐģĐ¸Ņ†", + "nightly_tasks_database_cleanup_setting": "Đ—Đ°Đ´Đ°Ņ‡Đ¸ ĐžŅ‡Đ¸ŅŅ‚Đēи ĐąĐ°ĐˇŅ‹ даĐŊĐŊҋ҅", + "nightly_tasks_database_cleanup_setting_description": "ĐŖĐ´Đ°ĐģĐĩĐŊиĐĩ ŅŅ‚Đ°Ņ€Ņ‹Ņ… и йОĐģĐĩĐĩ ĐŊĐĩĐŊ҃ĐļĐŊҋ҅ СаĐŋĐ¸ŅĐĩĐš иС ĐąĐ°ĐˇŅ‹ даĐŊĐŊҋ҅", + "nightly_tasks_generate_memories_setting": "ХОСдаĐŊиĐĩ Đ˛ĐžŅĐŋĐžĐŧиĐŊаĐŊиК", + "nightly_tasks_generate_memories_setting_description": "ХОСдаĐŊиĐĩ ĐŊĐžĐ˛Ņ‹Ņ… Đ˛ĐžŅĐŋĐžĐŧиĐŊаĐŊиК иС ŅŅƒŅ‰ĐĩŅŅ‚Đ˛ŅƒŅŽŅ‰Đ¸Ņ… ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛", + "nightly_tasks_missing_thumbnails_setting": "ХОСдаĐŊиĐĩ ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‰Đ¸Ņ… ĐŧиĐŊĐ¸Đ°Ņ‚ŅŽŅ€", + "nightly_tasks_missing_thumbnails_setting_description": "ДобавĐģĐĩĐŊиĐĩ ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ĐąĐĩС ĐŧиĐŊĐ¸Đ°Ņ‚ŅŽŅ€ в ĐžŅ‡ĐĩŅ€ĐĩĐ´ŅŒ Đ´ĐģŅ Đ¸Ņ… ŅĐžĐˇĐ´Đ°ĐŊĐ¸Ņ", + "nightly_tasks_settings": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи ĐŊĐžŅ‡ĐŊҋ҅ ĐˇĐ°Đ´Đ°Ņ‡", + "nightly_tasks_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊĐžŅ‡ĐŊŅ‹Đŧи Ņ€ĐĩĐŗĐģаĐŧĐĩĐŊŅ‚ĐŊŅ‹Đŧи ĐˇĐ°Đ´Đ°Ņ‡Đ°Đŧи", + "nightly_tasks_start_time_setting": "Đ’Ņ€ĐĩĐŧŅ ĐŊĐ°Ņ‡Đ°Đģа", + "nightly_tasks_start_time_setting_description": "Đ’Ņ€ĐĩĐŧŅ, ĐēĐžĐŗĐ´Đ° ҁĐĩŅ€Đ˛ĐĩŅ€ ĐŊĐ°Ņ‡Đ¸ĐŊаĐĩŅ‚ Đ˛Ņ‹ĐŋĐžĐģĐŊŅŅ‚ŅŒ ĐŊĐžŅ‡ĐŊŅ‹Đĩ ĐˇĐ°Đ´Đ°Ņ‡Đ¸", + "nightly_tasks_sync_quota_usage_setting": "ХиĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Ņ ĐēĐ˛ĐžŅ‚ Ņ…Ņ€Đ°ĐŊиĐģĐ¸Ņ‰Đ°", + "nightly_tasks_sync_quota_usage_setting_description": "ОбĐŊОвĐģĐĩĐŊиĐĩ ĐēĐ˛ĐžŅ‚Ņ‹ Ņ…Ņ€Đ°ĐŊиĐģĐ¸Ņ‰Đ° ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ ĐŊа ĐžŅĐŊОвĐĩ аĐēŅ‚ŅƒĐ°ĐģҌĐŊҋ҅ даĐŊĐŊҋ҅", "no_paths_added": "ĐŸŅƒŅ‚Đ¸ ĐŊĐĩ дОйавĐģĐĩĐŊŅ‹", "no_pattern_added": "ШайĐģĐžĐŊ ĐŊĐĩ дОйавĐģĐĩĐŊ", "note_apply_storage_label_previous_assets": "ĐŸŅ€Đ¸ĐŧĐĩŅ‡Đ°ĐŊиĐĩ: Đ§Ņ‚ĐžĐąŅ‹ ĐŋŅ€Đ¸ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŧĐĩŅ‚Đē҃ Ņ…Ņ€Đ°ĐŊиĐģĐ¸Ņ‰Đ° Đē Ņ€Đ°ĐŊĐĩĐĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐĩĐŊĐŊŅ‹Đŧ ĐžĐąŅŠĐĩĐēŅ‚Đ°Đŧ, СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đĩ", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "URI Ņ€ĐĩĐ´Đ¸Ņ€ĐĩĐēŅ‚Đ° Đ´ĐģŅ ĐŧОйиĐģҌĐŊҋ҅", "oauth_mobile_redirect_uri_override": "ПĐĩŅ€ĐĩĐŊаĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ URI Đ´ĐģŅ ĐŧОйиĐģҌĐŊҋ҅ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛", "oauth_mobile_redirect_uri_override_description": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚Đĩ, ĐĩҁĐģи ĐŋĐžŅŅ‚Đ°Đ˛Ņ‰Đ¸Đē OAuth ĐŊĐĩ Ņ€Đ°ĐˇŅ€ĐĩŅˆĐ°ĐĩŅ‚ Đ¸ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°ĐŊиĐĩ ĐŧОйиĐģҌĐŊĐžĐŗĐž URI, ĐŊаĐŋŅ€Đ¸ĐŧĐĩŅ€, ''{callback}''", + "oauth_role_claim": "ĐŖŅ‚Đ˛ĐĩŅ€ĐļĐ´ĐĩĐŊиĐĩ Ņ€ĐžĐģи", + "oauth_role_claim_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐĩҁĐēĐžĐĩ ĐŋŅ€ĐĩĐ´ĐžŅŅ‚Đ°Đ˛ĐģĐĩĐŊиĐĩ Đ´ĐžŅŅ‚ŅƒĐŋа адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ° ĐŊа ĐžŅĐŊОвĐĩ ĐŊаĐģĐ¸Ņ‡Đ¸Ņ ŅŅ‚ĐžĐŗĐž ŅƒŅ‚Đ˛ĐĩŅ€ĐļĐ´ĐĩĐŊĐ¸Ņ. ĐŖŅ‚Đ˛ĐĩŅ€ĐļĐ´ĐĩĐŊиĐĩ ĐŧĐžĐļĐĩŅ‚ иĐŧĐĩŅ‚ŅŒ СĐŊĐ°Ņ‡ĐĩĐŊиĐĩ 'user' иĐģи 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи Đ˛Ņ…ĐžĐ´Đ° ҇ĐĩŅ€ĐĩС OAuth", "oauth_settings_more_details": "ДĐģŅ ĐŋĐžĐģŅƒŅ‡ĐĩĐŊĐ¸Ņ Đ´ĐžĐŋĐžĐģĐŊĐ¸Ņ‚ĐĩĐģҌĐŊОК иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Đš Ой ŅŅ‚ĐžĐš Ņ„ŅƒĐŊĐēŅ†Đ¸Đ¸ ĐžĐąŅ€Đ°Ņ‚Đ¸Ņ‚ĐĩҁҌ Đē Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Đ¸Đ¸.", @@ -212,7 +228,7 @@ "password_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēаĐŧи Đ˛Ņ…ĐžĐ´Đ° ĐŋĐž ĐŋĐ°Ņ€ĐžĐģŅŽ", "paths_validated_successfully": "Đ’ŅĐĩ ĐŋŅƒŅ‚Đ¸ ҃ҁĐŋĐĩ҈ĐŊĐž ĐŋŅ€ĐžŅˆĐģи ĐŋŅ€ĐžĐ˛ĐĩŅ€Đē҃", "person_cleanup_job": "ĐžŅ‡Đ¸ŅŅ‚Đēа ĐŋĐĩŅ€ŅĐžĐŊŅ‹", - "quota_size_gib": "РаСĐŧĐĩŅ€ ĐēĐ˛ĐžŅ‚Ņ‹ (ГБ)", + "quota_size_gib": "РаСĐŧĐĩŅ€ ĐēĐ˛ĐžŅ‚Ņ‹ (GiB)", "refreshing_all_libraries": "ОбĐŊОвĐģĐĩĐŊиĐĩ Đ˛ŅĐĩŅ… йийĐģĐ¸ĐžŅ‚ĐĩĐē", "registration": "Đ ĐĩĐŗĐ¸ŅŅ‚Ņ€Đ°Ņ†Đ¸Ņ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", "registration_description": "ПĐĩŅ€Đ˛Ņ‹Đš ĐˇĐ°Ņ€ĐĩĐŗĐ¸ŅŅ‚Ņ€Đ¸Ņ€ĐžĐ˛Đ°ĐŊĐŊŅ‹Đš ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģҌ ĐąŅƒĐ´ĐĩŅ‚ ĐŊаСĐŊĐ°Ņ‡ĐĩĐŊ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€ĐžĐŧ. В даĐģҌĐŊĐĩĐšŅˆĐĩĐŧ ŅŅ‚ĐžĐš ŅƒŅ‡ĐĩŅ‚ĐŊОК СаĐŋĐ¸ŅĐ¸ ĐąŅƒĐ´ĐĩŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐž ŅĐžĐˇĐ´Đ°ĐŊиĐĩ Đ´ĐžĐŋĐžĐģĐŊĐ¸Ņ‚ĐĩĐģҌĐŊҋ҅ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš и ҃ĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ.", @@ -225,7 +241,7 @@ "server_external_domain_settings": "ВĐŊĐĩ҈ĐŊиК Đ´ĐžĐŧĐĩĐŊ", "server_external_domain_settings_description": "ДоĐŧĐĩĐŊ Đ´ĐģŅ ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊҋ҅ ҁҁҋĐģĐžĐē, вĐēĐģŅŽŅ‡Đ°Ņ http(s)://", "server_public_users": "ĐŸŅƒĐąĐģĐ¸Ņ‡ĐŊŅ‹Đĩ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģи", - "server_public_users_description": "ĐžŅ‚ĐžĐąŅ€Đ°ĐļĐ°Ņ‚ŅŒ Đ˛ŅĐĩŅ… ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš (иĐŧĐĩĐŊа и email) Đ´ĐģŅ дОйавĐģĐĩĐŊĐ¸Ņ в ĐžĐąŅ‰Đ¸Đĩ аĐģŅŒĐąĐžĐŧŅ‹. ĐšĐžĐŗĐ´Đ° ĐžŅ‚ĐēĐģŅŽŅ‡ĐĩĐŊĐž, ҁĐŋĐ¸ŅĐžĐē ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš ĐąŅƒĐ´ĐĩŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋĐĩĐŊ Ņ‚ĐžĐģҌĐēĐž адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°Đŧ.", + "server_public_users_description": "Đ’Ņ‹Đ˛ĐžĐ´Đ¸Ņ‚ŅŒ ҁĐŋĐ¸ŅĐžĐē ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš (иĐŧĐĩĐŊа и email) в ĐžĐąŅ‰Đ¸Ņ… аĐģŅŒĐąĐžĐŧĐ°Ņ…. ĐšĐžĐŗĐ´Đ° ĐžŅ‚ĐēĐģŅŽŅ‡ĐĩĐŊĐž, ҁĐŋĐ¸ŅĐžĐē Đ´ĐžŅŅ‚ŅƒĐŋĐĩĐŊ Ņ‚ĐžĐģҌĐēĐž адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°Đŧ, ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģи ҁĐŧĐžĐŗŅƒŅ‚ Đ´ĐĩĐģĐ¸Ņ‚ŅŒŅŅ Ņ‚ĐžĐģҌĐēĐž ҁҁҋĐģĐēОК.", "server_settings": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "server_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēаĐŧи ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "server_welcome_message": "ĐŸŅ€Đ¸Đ˛ĐĩŅ‚ŅŅ‚Đ˛ĐĩĐŊĐŊĐžĐĩ ŅĐžĐžĐąŅ‰ĐĩĐŊиĐĩ", @@ -357,10 +373,12 @@ "admin_password": "ĐŸĐ°Ņ€ĐžĐģҌ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", "administration": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ", "advanced": "Đ Đ°ŅŅˆĐ¸Ņ€ĐĩĐŊĐŊŅ‹Đĩ", + "advanced_settings_beta_timeline_subtitle": "ПоĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ ĐŊĐžĐ˛Ņ‹Đš Ņ„ŅƒĐŊĐēŅ†Đ¸ĐžĐŊаĐģ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ", + "advanced_settings_beta_timeline_title": "БĐĩŅ‚Đ°-вĐĩŅ€ŅĐ¸Ņ Đ˛Ņ€ĐĩĐŧĐĩĐŊĐŊОК ҈ĐēаĐģŅ‹", "advanced_settings_enable_alternate_media_filter_subtitle": "Đ˜ŅĐŋĐžĐģŅŒĐˇŅƒĐšŅ‚Đĩ ŅŅ‚ĐžŅ‚ ĐŋĐ°Ņ€Đ°ĐŧĐĩ҂Ҁ Đ´ĐģŅ Ņ„Đ¸ĐģŅŒŅ‚Ņ€Đ°Ņ†Đ¸Đ¸ ĐŧĐĩĐ´Đ¸Đ°Ņ„Đ°ĐšĐģОв вО Đ˛Ņ€ĐĩĐŧŅ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Đ¸ ĐŊа ĐžŅĐŊОвĐĩ аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊҋ҅ ĐēŅ€Đ¸Ņ‚ĐĩŅ€Đ¸Đĩв. ĐŸŅ€ĐžĐąŅƒĐšŅ‚Đĩ Ņ‚ĐžĐģҌĐēĐž в Ņ‚ĐžĐŧ ҁĐģŅƒŅ‡Đ°Đĩ, ĐĩҁĐģи ҃ Đ˛Đ°Ņ ĐĩŅŅ‚ŅŒ ĐŋŅ€ĐžĐąĐģĐĩĐŧŅ‹ ҁ ОйĐŊĐ°Ņ€ŅƒĐļĐĩĐŊиĐĩĐŧ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩĐŧ Đ˛ŅĐĩŅ… аĐģŅŒĐąĐžĐŧОв.", "advanced_settings_enable_alternate_media_filter_title": "[ЭКСПЕРИМЕНĐĸАЛĐŦНО] Đ˜ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°ĐŊиĐĩ Ņ„Đ¸ĐģŅŒŅ‚Ņ€Đ° ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Đ¸ аĐģŅŒĐąĐžĐŧОв аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊҋ҅ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛", "advanced_settings_log_level_title": "ĐŖŅ€ĐžĐ˛ĐĩĐŊҌ ĐģĐžĐŗĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐ¸Ņ: {level}", - "advanced_settings_prefer_remote_subtitle": "НĐĩĐēĐžŅ‚ĐžŅ€Ņ‹Đĩ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ° ĐžŅ‡ĐĩĐŊҌ ĐŧĐĩĐ´ĐģĐĩĐŊĐŊĐž ĐˇĐ°ĐŗŅ€ŅƒĐļĐ°ŅŽŅ‚ ĐģĐžĐēаĐģҌĐŊŅ‹Đĩ Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ. АĐēŅ‚Đ¸Đ˛Đ¸Ņ€ŅƒĐšŅ‚Đĩ ŅŅ‚Ņƒ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐē҃, Ņ‡Ņ‚ĐžĐąŅ‹ Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ Đ˛ŅĐĩĐŗĐ´Đ° ĐˇĐ°ĐŗŅ€ŅƒĐļаĐģĐ¸ŅŅŒ ҁ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°.", + "advanced_settings_prefer_remote_subtitle": "НĐĩĐēĐžŅ‚ĐžŅ€Ņ‹Đĩ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ° ĐžŅ‡ĐĩĐŊҌ ĐŧĐĩĐ´ĐģĐĩĐŊĐŊĐž ĐˇĐ°ĐŗŅ€ŅƒĐļĐ°ŅŽŅ‚ ĐģĐžĐēаĐģҌĐŊŅ‹Đĩ ĐŧиĐŊĐ¸Đ°Ņ‚ŅŽŅ€Ņ‹. АĐēŅ‚Đ¸Đ˛Đ¸Ņ€ŅƒĐšŅ‚Đĩ ŅŅ‚Ņƒ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐē҃, Ņ‡Ņ‚ĐžĐąŅ‹ Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ Đ˛ŅĐĩĐŗĐ´Đ° ĐˇĐ°ĐŗŅ€ŅƒĐļаĐģĐ¸ŅŅŒ ҁ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°.", "advanced_settings_prefer_remote_title": "ĐŸŅ€ĐĩĐ´ĐŋĐžŅ‡Đ¸Ņ‚Đ°Ņ‚ŅŒ Ņ„ĐžŅ‚Đž ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€Đĩ", "advanced_settings_proxy_headers_subtitle": "ОĐŋŅ€ĐĩĐ´ĐĩĐģĐ¸Ņ‚Đĩ ĐˇĐ°ĐŗĐžĐģОвĐēи ĐŋŅ€ĐžĐēŅĐ¸-ҁĐĩŅ€Đ˛ĐĩŅ€Đ°, ĐēĐžŅ‚ĐžŅ€Ņ‹Đĩ Immich Đ´ĐžĐģĐļĐĩĐŊ ĐžŅ‚ĐŋŅ€Đ°Đ˛ĐģŅŅ‚ŅŒ ҁ ĐēаĐļĐ´Ņ‹Đŧ ҁĐĩŅ‚ĐĩĐ˛Ņ‹Đŧ СаĐŋŅ€ĐžŅĐžĐŧ", "advanced_settings_proxy_headers_title": "Đ—Đ°ĐŗĐžĐģОвĐēи ĐŋŅ€ĐžĐēŅĐ¸", @@ -388,7 +406,8 @@ "album_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ аĐģŅŒĐąĐžĐŧа", "album_remove_user": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ?", "album_remove_user_confirmation": "Đ’Ņ‹ ŅƒĐ˛ĐĩŅ€ĐĩĐŊŅ‹, Ņ‡Ņ‚Đž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ {user}?", - "album_share_no_users": "ĐŸĐžŅ…ĐžĐļĐĩ, Đ˛Ņ‹ ĐŋОдĐĩĐģиĐģĐ¸ŅŅŒ ŅŅ‚Đ¸Đŧ аĐģŅŒĐąĐžĐŧĐžĐŧ ŅĐž Đ˛ŅĐĩĐŧи ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅĐŧи иĐģи ҃ Đ˛Đ°Ņ ĐŊĐĩŅ‚ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš, ҁ ĐēĐžŅ‚ĐžŅ€Ņ‹Đŧи ĐŧĐžĐļĐŊĐž ĐŋОдĐĩĐģĐ¸Ņ‚ŅŒŅŅ.", + "album_search_not_found": "НĐĩ ĐŊаКдĐĩĐŊĐž аĐģŅŒĐąĐžĐŧОв ĐŋĐž Đ˛Đ°ŅˆĐĩĐŧ҃ СаĐŋŅ€ĐžŅŅƒ", + "album_share_no_users": "НĐĩŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋĐŊҋ҅ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš, ҁ ĐēĐžŅ‚ĐžŅ€Ņ‹Đŧи ĐŧĐžĐļĐŊĐž ĐŋОдĐĩĐģĐ¸Ņ‚ŅŒŅŅ аĐģŅŒĐąĐžĐŧĐžĐŧ.", "album_updated": "АĐģŅŒĐąĐžĐŧ ОйĐŊОвĐģŅ‘ĐŊ", "album_updated_setting_description": "ПоĐģŅƒŅ‡Đ°Ņ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиĐĩ ĐŋĐž ŅĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊОК ĐŋĐžŅ‡Ņ‚Đĩ ĐŋŅ€Đ¸ дОйавĐģĐĩĐŊии ĐŊĐžĐ˛Ņ‹Ņ… Ņ€ĐĩŅŅƒŅ€ŅĐžĐ˛ в ĐžĐąŅ‰Đ¸Đš аĐģŅŒĐąĐžĐŧ", "album_user_left": "Đ’Ņ‹ ĐŋĐžĐēиĐŊ҃Đģи {album}", @@ -407,14 +426,15 @@ "albums_default_sort_order": "ĐŸĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚Đ¸Ņ€ĐžĐ˛Đēи в аĐģŅŒĐąĐžĐŧĐ°Ņ… ĐŋĐž ҃ĐŧĐžĐģŅ‡Đ°ĐŊĐ¸ŅŽ", "albums_default_sort_order_description": "ПĐĩŅ€Đ˛ĐžĐŊĐ°Ņ‡Đ°ĐģҌĐŊŅ‹Đš ĐŋĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚Đ¸Ņ€ĐžĐ˛Đēи, ŅƒŅŅ‚Đ°ĐŊавĐģиваĐĩĐŧŅ‹Đš в ĐŊĐžĐ˛Ņ‹Ņ… аĐģŅŒĐąĐžĐŧĐ°Ņ….", "albums_feature_description": "КоĐģĐģĐĩĐēŅ†Đ¸Đ¸ Ņ„ĐžŅ‚Đž и видĐĩĐž, ĐēĐžŅ‚ĐžŅ€Ņ‹Đŧи ĐŧĐžĐļĐŊĐž Đ´ĐĩĐģĐ¸Ņ‚ŅŒŅŅ ҁ Đ´Ņ€ŅƒĐŗĐ¸Đŧи ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅĐŧи.", + "albums_on_device_count": "АĐģŅŒĐąĐžĐŧŅ‹ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ ({count})", "all": "Đ’ŅĐĩ", "all_albums": "Đ’ŅĐĩ аĐģŅŒĐąĐžĐŧŅ‹", "all_people": "Đ’ŅĐĩ ĐģŅŽĐ´Đ¸", "all_videos": "Đ’ŅĐĩ видĐĩĐž", "allow_dark_mode": "Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ Ņ‚ĐĩĐŧĐŊŅ‹Đš Ņ€ĐĩĐļиĐŧ", "allow_edits": "Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ Ņ€ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ", - "allow_public_user_to_download": "Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ ҁĐēĐ°Ņ‡Đ¸Đ˛Đ°ĐŊиĐĩ ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊŅ‹Đŧ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅĐŧ", - "allow_public_user_to_upload": "Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊŅ‹Đŧ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅĐŧ ĐˇĐ°ĐŗŅ€ŅƒĐļĐ°Ņ‚ŅŒ Ņ„Đ°ĐšĐģŅ‹", + "allow_public_user_to_download": "Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ ҁĐēĐ°Ņ‡Đ¸Đ˛Đ°ĐŊиĐĩ", + "allow_public_user_to_upload": "Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ дОйавĐģĐĩĐŊиĐĩ Ņ„Đ°ĐšĐģОв", "alt_text_qr_code": "QR-ĐēОд", "anti_clockwise": "ĐŸŅ€ĐžŅ‚Đ¸Đ˛ Ņ‡Đ°ŅĐžĐ˛ĐžĐš", "api_key": "API ĐēĐģŅŽŅ‡", @@ -427,6 +447,7 @@ "app_settings": "ĐŸĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ", "appears_in": "ДобавĐģĐĩĐŊĐž в", "archive": "ĐŅ€Ņ…Đ¸Đ˛", + "archive_action_prompt": "{count} дОйавĐģĐĩĐŊĐž в ĐŅ€Ņ…Đ¸Đ˛", "archive_or_unarchive_photo": "ĐŅ€Ņ…Đ¸Đ˛Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ иĐģи Ņ€Đ°ĐˇĐ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Ņ„ĐžŅ‚Đž", "archive_page_no_archived_assets": "В Đ°Ņ€Ņ…Đ¸Đ˛Đĩ ҁĐĩĐšŅ‡Đ°Ņ ĐŋŅƒŅŅ‚Đž", "archive_page_title": "ĐŅ€Ņ…Đ¸Đ˛ ({count})", @@ -464,7 +485,6 @@ "assets": "ĐžĐąŅŠĐĩĐē҂ҋ", "assets_added_count": "{count, plural, one {ДобавĐģĐĩĐŊ # ĐžĐąŅŠĐĩĐēŅ‚} many {ДобавĐģĐĩĐŊĐž # ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛} other {ДобавĐģĐĩĐŊĐž # ĐžĐąŅŠĐĩĐēŅ‚Đ°}}", "assets_added_to_album_count": "В аĐģŅŒĐąĐžĐŧ {count, plural, one {дОйавĐģĐĩĐŊ # ĐžĐąŅŠĐĩĐēŅ‚} many {дОйавĐģĐĩĐŊĐž # ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛} other {дОйавĐģĐĩĐŊĐž # ĐžĐąŅŠĐĩĐēŅ‚Đ°}}", - "assets_added_to_name_count": "{count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚ дОйавĐģĐĩĐŊ} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ дОйавĐģĐĩĐŊĐž} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ° дОйавĐģĐĩĐŊĐž}} в {hasName, select, true {аĐģŅŒĐąĐžĐŧ {name}} other {ĐŊĐžĐ˛Ņ‹Đš аĐģŅŒĐąĐžĐŧ}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {ĐžĐąŅŠĐĩĐēŅ‚ ĐŊĐĩ ĐŧĐžĐļĐĩŅ‚ ĐąŅ‹Ņ‚ŅŒ дОйавĐģĐĩĐŊ} other {ĐžĐąŅŠĐĩĐē҂ҋ ĐŊĐĩ ĐŧĐžĐŗŅƒŅ‚ ĐąŅ‹Ņ‚ŅŒ дОйавĐģĐĩĐŊŅ‹}} в аĐģŅŒĐąĐžĐŧ", "assets_count": "{count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ°}}", "assets_deleted_permanently": "{count} ĐžĐąŅŠĐĩĐēŅ‚(Ов) ŅƒĐ´Đ°ĐģĐĩĐŊĐž ĐŊĐ°Đ˛ŅĐĩĐŗĐ´Đ°", @@ -553,6 +573,8 @@ "backup_options_page_title": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐžĐĩ ĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ", "backup_setting_subtitle": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēа аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŗĐž и Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐ¸Ņ", "backward": "Назад", + "beta_sync": "ĐĄŅ‚Đ°Ņ‚ŅƒŅ ĐąĐĩŅ‚Đ°-ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Đ¸", + "beta_sync_subtitle": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊОвОК ŅĐ¸ŅŅ‚ĐĩĐŧОК ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Đ¸", "biometric_auth_enabled": "БиоĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐĩҁĐēĐ°Ņ Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Ņ вĐēĐģŅŽŅ‡ĐĩĐŊа", "biometric_locked_out": "ВаĐŧ СаĐēҀҋ҂ Đ´ĐžŅŅ‚ŅƒĐŋ Đē йиОĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐĩҁĐēОК Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Đ¸", "biometric_no_options": "БиоĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐĩҁĐēĐ°Ņ Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Ņ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа", @@ -570,7 +592,7 @@ "cache_settings_clear_cache_button": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ ĐēŅŅˆ", "cache_settings_clear_cache_button_title": "ĐžŅ‡Đ¸Ņ‰Đ°ĐĩŅ‚ ĐēŅŅˆ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ. Đ­Ņ‚Đž ĐŊĐĩĐŗĐ°Ņ‚Đ¸Đ˛ĐŊĐž ĐŋОвĐģĐ¸ŅĐĩŅ‚ ĐŊа ĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐžĐ´Đ¸Ņ‚ĐĩĐģҌĐŊĐžŅŅ‚ŅŒ, ĐŋĐžĐēа ĐēŅŅˆ ĐŊĐĩ ĐąŅƒĐ´ĐĩŅ‚ ŅĐžĐˇĐ´Đ°ĐŊ СаĐŊОвО.", "cache_settings_duplicated_assets_clear_button": "ОЧИСĐĸИĐĸĐŦ", - "cache_settings_duplicated_assets_subtitle": "Đ¤ĐžŅ‚Đž и видĐĩĐž, СаĐŊĐĩҁĐĩĐŊĐŊŅ‹Đĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩĐŧ в ҇ĐĩŅ€ĐŊŅ‹Đš ҁĐŋĐ¸ŅĐžĐē", + "cache_settings_duplicated_assets_subtitle": "Đ¤ĐžŅ‚Đž и видĐĩĐž, ĐŋŅ€ĐžĐŋ҃ҁĐēаĐĩĐŧŅ‹Đĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩĐŧ", "cache_settings_duplicated_assets_title": "Đ”ŅƒĐąĐģĐ¸Ņ€ŅƒŅŽŅ‰Đ¸ĐĩŅŅ ĐžĐąŅŠĐĩĐē҂ҋ ({count})", "cache_settings_statistics_album": "МиĐŊĐ¸Đ°Ņ‚ŅŽŅ€Ņ‹ йийĐģĐ¸ĐžŅ‚ĐĩĐēи", "cache_settings_statistics_full": "ПоĐģĐŊŅ‹Đĩ Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ", @@ -587,6 +609,7 @@ "cancel": "ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ", "cancel_search": "ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŋĐžĐ¸ŅĐē", "canceled": "ĐžŅ‚ĐŧĐĩĐŊĐĩĐŊĐž", + "canceling": "ĐžŅ‚ĐŧĐĩĐŊа", "cannot_merge_people": "НĐĩвОСĐŧĐžĐļĐŊĐž ĐžĐąŅŠĐĩдиĐŊĐ¸Ņ‚ŅŒ ĐģŅŽĐ´ĐĩĐš", "cannot_undo_this_action": "Đ­Ņ‚Đž Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Đĩ ĐŊĐĩĐģŅŒĐˇŅ ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ!", "cannot_update_the_description": "НĐĩвОСĐŧĐžĐļĐŊĐž ОйĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ", @@ -595,7 +618,7 @@ "change_date": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Đ´Đ°Ņ‚Ņƒ", "change_description": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ", "change_display_order": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŋĐžŅ€ŅĐ´ĐžĐē ĐžŅ‚ĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ", - "change_expiration_time": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Đ˛Ņ€ĐĩĐŧŅ ĐžĐēĐžĐŊŅ‡Đ°ĐŊĐ¸Ņ", + "change_expiration_time": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ŅŅ€ĐžĐē Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ", "change_location": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", "change_name": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ иĐŧŅ", "change_name_successfully": "ИĐŧŅ ҃ҁĐŋĐĩ҈ĐŊĐž иСĐŧĐĩĐŊĐĩĐŊĐž", @@ -671,7 +694,7 @@ "copy_link": "КоĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ҁҁҋĐģĐē҃", "copy_link_to_clipboard": "ĐĄĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ҁҁҋĐģĐē҃ в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧĐĩĐŊа", "copy_password": "ĐĄĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ", - "copy_to_clipboard": "ĐĄĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧĐĩĐŊа", + "copy_to_clipboard": "ĐĄĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēи в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧĐĩĐŊа", "country": "ĐĄŅ‚Ņ€Đ°ĐŊа", "cover": "ОбĐģĐžĐļĐēа", "covers": "ОбĐģĐžĐļĐēи", @@ -703,7 +726,7 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "ĐĸŅ‘ĐŧĐŊĐ°Ņ", - "darkTheme": "ПĐĩŅ€ĐĩĐēĐģŅŽŅ‡ĐĩĐŊиĐĩ Ņ‚ĐĩĐŧĐŊОК Ņ‚ĐĩĐŧŅ‹", + "dark_theme": "ĐĸŅ‘ĐŧĐŊĐ°Ņ Ņ‚ĐĩĐŧа", "date_after": "Đ”Đ°Ņ‚Đ° ĐŋĐžŅĐģĐĩ", "date_and_time": "Đ”Đ°Ņ‚Đ° и Đ’Ņ€ĐĩĐŧŅ", "date_before": "Đ”Đ°Ņ‚Đ° Đ´Đž", @@ -713,12 +736,13 @@ "day": "ДĐĩĐŊҌ", "deduplicate_all": "ĐŖĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹", "deduplication_criteria_1": "РаСĐŧĐĩŅ€ Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ в ĐąĐ°ĐšŅ‚Đ°Ņ…", - "deduplication_criteria_2": "ĐŸĐžĐ´ŅŅ‡ĐĩŅ‚ даĐŊĐŊҋ҅ EXIF", + "deduplication_criteria_2": "КоĐģĐ¸Ņ‡ĐĩŅŅ‚Đ˛Đž EXIF даĐŊĐŊҋ҅", "deduplication_info": "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Đž Đ´ĐĩĐ´ŅƒĐŋĐģиĐēĐ°Ņ†Đ¸Đ¸", - "deduplication_info_description": "ДĐģŅ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐĩҁĐēĐžĐŗĐž ĐŋŅ€ĐĩĐ´Đ˛Đ°Ņ€Đ¸Ņ‚ĐĩĐģҌĐŊĐžĐŗĐž Đ˛Ņ‹ĐąĐžŅ€Đ° ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ и ĐŧĐ°ŅŅĐžĐ˛ĐžĐŗĐž ŅƒĐ´Đ°ĐģĐĩĐŊĐ¸Ņ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚ĐžĐ˛ ĐŧŅ‹ Ņ€Đ°ŅŅĐŧĐžŅ‚Ņ€Đ¸Đŧ:", + "deduplication_info_description": "ДĐģŅ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐĩҁĐēĐžĐŗĐž Đ˛Ņ‹ĐąĐžŅ€Đ° ĐģŅƒŅ‡ŅˆĐ¸Ņ… ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ҁҀĐĩди Đ´ŅƒĐąĐģиĐēĐ°Ņ‚ĐžĐ˛ аĐŊаĐģĐ¸ĐˇĐ¸Ņ€ŅƒĐĩŅ‚ŅŅ ҁĐģĐĩĐ´ŅƒŅŽŅ‰Đ°Ņ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ:", "default_locale": "Đ”Đ°Ņ‚Đ° и Đ˛Ņ€ĐĩĐŧŅ ĐŋĐž ҃ĐŧĐžĐģŅ‡Đ°ĐŊĐ¸ŅŽ", "default_locale_description": "Đ˜ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ Ņ„ĐžŅ€ĐŧĐ°Ņ‚ Đ´Đ°Ņ‚Ņ‹ и Đ˛Ņ€ĐĩĐŧĐĩĐŊи в ŅĐžĐžŅ‚Đ˛ĐĩŅ‚ŅŅ‚Đ˛Đ¸Đ¸ ҁ ŅĐˇŅ‹ĐēĐžĐ˛Ņ‹Đŧ ŅŅ‚Đ°ĐŊĐ´Đ°Ņ€Ņ‚ĐžĐŧ Đ˛Đ°ŅˆĐĩĐŗĐž ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ°", "delete": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ", + "delete_action_prompt": "{count} ŅƒĐ´Đ°ĐģĐĩĐŊĐž ĐŊĐ°Đ˛ŅĐĩĐŗĐ´Đ°", "delete_album": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ", "delete_api_key_prompt": "Đ’Ņ‹ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ‚ĐĩĐģҌĐŊĐž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ŅŅ‚ĐžŅ‚ API ĐēĐģŅŽŅ‡?", "delete_dialog_alert": "Đ­Ņ‚Đ¸ ŅĐģĐĩĐŧĐĩĐŊ҂ҋ ĐąŅƒĐ´ŅƒŅ‚ ĐąĐĩĐˇĐ˛ĐžĐˇĐ˛Ņ€Đ°Ņ‚ĐŊĐž ŅƒĐ´Đ°ĐģĐĩĐŊŅ‹ ҁ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°, а Ņ‚Đ°ĐēĐļĐĩ ҁ Đ˛Đ°ŅˆĐĩĐŗĐž ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ°", @@ -732,6 +756,7 @@ "delete_key": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ĐēĐģŅŽŅ‡", "delete_library": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ йийĐģĐ¸ĐžŅ‚ĐĩĐē҃", "delete_link": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ҁҁҋĐģĐē҃", + "delete_local_action_prompt": "{count} ŅƒĐ´Đ°ĐģĐĩĐŊĐž ĐģĐžĐēаĐģҌĐŊĐž", "delete_local_dialog_ok_backed_up_only": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ Ņ‚ĐžĐģҌĐēĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ‹Đĩ ĐēĐžĐŋии", "delete_local_dialog_ok_force": "Đ’ŅĐĩ Ņ€Đ°Đ˛ĐŊĐž ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ", "delete_others": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ĐžŅŅ‚Đ°ĐģҌĐŊŅ‹Đĩ", @@ -745,6 +770,7 @@ "description": "ОĐŋĐ¸ŅĐ°ĐŊиĐĩ", "description_input_hint_text": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ...", "description_input_submit_error": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ОйĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ, ĐŋŅ€ĐžĐ˛ĐĩŅ€ŅŒŅ‚Đĩ ĐģĐžĐŗĐ¸, Ņ‡Ņ‚ĐžĐąŅ‹ ŅƒĐˇĐŊĐ°Ņ‚ŅŒ ĐŋŅ€Đ¸Ņ‡Đ¸ĐŊ҃", + "deselect_all": "ĐĄĐŊŅŅ‚ŅŒ Đ˛Ņ‹Đ´ĐĩĐģĐĩĐŊиĐĩ", "details": "ĐŸĐžĐ´Ņ€ĐžĐąĐŊĐžŅŅ‚Đ¸", "direction": "НаĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ", "disabled": "ĐžŅ‚ĐēĐģŅŽŅ‡ĐĩĐŊĐž", @@ -762,6 +788,7 @@ "documentation": "ДоĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Đ¸Ņ", "done": "Đ“ĐžŅ‚ĐžĐ˛Đž", "download": "ĐĄĐēĐ°Ņ‡Đ°Ņ‚ŅŒ", + "download_action_prompt": "Đ—Đ°ĐŗŅ€ŅƒĐļĐ°ŅŽŅ‚ŅŅ {count} ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛", "download_canceled": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа ĐžŅ‚ĐŧĐĩĐŊĐĩĐŊа", "download_complete": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа ĐžĐēĐžĐŊ҇ĐĩĐŊа", "download_enqueue": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа в ĐžŅ‡ĐĩŅ€Đĩди", @@ -783,7 +810,7 @@ "downloading_media": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа ĐŧĐĩдиа", "drop_files_to_upload": "ПĐĩŅ€ĐĩĐŊĐĩŅĐ¸Ņ‚Đĩ Ņ„Đ°ĐšĐģŅ‹ в ĐģŅŽĐąĐžĐĩ ĐŧĐĩŅŅ‚Đž Đ´ĐģŅ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēи", "duplicates": "Đ”ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹", - "duplicates_description": "РаСйĐĩŅ€Đ¸Ņ‚ĐĩҁҌ ҁ ĐēаĐļдОК ĐŗŅ€ŅƒĐŋĐŋОК, ҃ĐēаСав, ĐēаĐēиĐĩ иС ĐŊĐ¸Ņ… ŅĐ˛ĐģŅŅŽŅ‚ŅŅ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Đ°Đŧи, ĐĩҁĐģи Ņ‚Đ°ĐēĐžĐ˛Ņ‹Đĩ иĐŧĐĩŅŽŅ‚ŅŅ", + "duplicates_description": "ĐŸŅ€ĐžŅĐŧĐžŅ‚Ņ€Đ¸Ņ‚Đĩ ĐŊаКдĐĩĐŊĐŊŅ‹Đĩ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹ и в ĐēаĐļдОК ĐŗŅ€ŅƒĐŋĐŋĐĩ ҃ĐēаĐļĐ¸Ņ‚Đĩ, ĐēаĐēиĐĩ ĐžĐąŅŠĐĩĐē҂ҋ ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ, а ĐēаĐēиĐĩ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ", "duration": "ĐŸŅ€ĐžĐ´ĐžĐģĐļĐ¸Ņ‚ĐĩĐģҌĐŊĐžŅŅ‚ŅŒ", "edit": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ", "edit_album": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ", @@ -799,12 +826,13 @@ "edit_key": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐēĐģŅŽŅ‡", "edit_link": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ҁҁҋĐģĐē҃", "edit_location": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", + "edit_location_action_prompt": "{count} ĐŧĐĩҁ҂ иСĐŧĐĩĐŊĐĩĐŊĐž", "edit_location_dialog_title": "МĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", "edit_name": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ иĐŧŅ", "edit_people": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐģŅŽĐ´ĐĩĐš", "edit_tag": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Ņ‚ĐĩĐŗ", "edit_title": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Đ—Đ°ĐŗĐžĐģОвОĐē", - "edit_user": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ", + "edit_user": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ", "edited": "ĐžŅ‚Ņ€ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊĐž", "editor": "Đ ĐĩдаĐēŅ‚ĐžŅ€", "editor_close_without_save_prompt": "ИСĐŧĐĩĐŊĐĩĐŊĐ¸Ņ ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ‚ ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊŅ‹", @@ -817,6 +845,7 @@ "empty_trash": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ ĐēĐžŅ€ĐˇĐ¸ĐŊ҃", "empty_trash_confirmation": "Đ’Ņ‹ ŅƒĐ˛ĐĩŅ€ĐĩĐŊŅ‹, Ņ‡Ņ‚Đž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ ĐēĐžŅ€ĐˇĐ¸ĐŊ҃? Đ’ŅĐĩ ĐžĐąŅŠĐĩĐē҂ҋ в ĐēĐžŅ€ĐˇĐ¸ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ‚ ĐŊĐ°Đ˛ŅĐĩĐŗĐ´Đ° ŅƒĐ´Đ°ĐģĐĩĐŊŅ‹ иС Immich.\nĐ’Ņ‹ ĐŊĐĩ ҁĐŧĐžĐļĐĩŅ‚Đĩ ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ŅŅ‚Đž Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Đĩ!", "enable": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ", + "enable_backup": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐĩ ĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ", "enable_biometric_auth_description": "ВвĐĩĐ´Đ¸Ņ‚Đĩ ŅĐ˛ĐžĐš PIN-ĐēОд Đ´ĐģŅ вĐēĐģŅŽŅ‡ĐĩĐŊĐ¸Ņ йиОĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐĩҁĐēОК Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Đ¸", "enabled": "ВĐēĐģŅŽŅ‡ĐĩĐŊĐž", "end_date": "Đ”Đ°Ņ‚Đ° ĐžĐēĐžĐŊŅ‡Đ°ĐŊĐ¸Ņ", @@ -967,7 +996,7 @@ "experimental_settings_subtitle": "Đ˜ŅĐŋĐžĐģŅŒĐˇŅƒĐšŅ‚Đĩ ĐŊа ŅĐ˛ĐžĐš ŅŅ‚Ņ€Đ°Ņ… и Ņ€Đ¸ŅĐē!", "experimental_settings_title": "Đ­ĐēҁĐŋĐĩŅ€Đ¸ĐŧĐĩĐŊŅ‚Đ°ĐģҌĐŊŅ‹Đĩ Ņ„ŅƒĐŊĐēŅ†Đ¸Đ¸", "expire_after": "Đ˜ŅŅ‚ĐĩĐēаĐĩŅ‚ ҇ĐĩŅ€ĐĩС", - "expired": "ĐĄŅ€ĐžĐē Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ Đ¸ŅŅ‚ĐĩĐē", + "expired": "ĐĄŅ€ĐžĐē Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ Đ¸ŅŅ‚Ņ‘Đē", "expires_date": "ĐĄŅ€ĐžĐē Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ Đ´Đž {date}", "explore": "ĐŸĐžĐ¸ŅĐē", "explorer": "ĐŸŅ€ĐžĐ˛ĐžĐ´ĐŊиĐē", @@ -984,6 +1013,7 @@ "failed_to_load_assets": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐē҂ҋ", "failed_to_load_folder": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēĐĩ ĐŋаĐŋĐēи", "favorite": "Đ˜ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ", + "favorite_action_prompt": "{count} дОйавĐģĐĩĐŊĐž в Đ˜ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ", "favorite_or_unfavorite_photo": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ иĐģи ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸ŅŽ иС Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐŗĐž", "favorites": "Đ˜ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ", "favorites_page_no_favorites": "В Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐŧ ҁĐĩĐšŅ‡Đ°Ņ ĐŋŅƒŅŅ‚Đž", @@ -1023,6 +1053,9 @@ "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": "ИĐŧŅ ĐˇĐ°ĐŗĐžĐģОвĐēа", @@ -1055,6 +1088,7 @@ "host": "ĐĨĐžŅŅ‚", "hour": "Đ§Đ°Ņ", "id": "ID", + "idle": "В ĐžĐļидаĐŊии", "ignore_icloud_photos": "ĐŸŅ€ĐžĐŋ҃ҁĐēĐ°Ņ‚ŅŒ Ņ„Đ°ĐšĐģŅ‹ иС iCloud", "ignore_icloud_photos_description": "НĐĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐ°Ņ‚ŅŒ Ņ„Đ°ĐšĐģŅ‹ в Immich, ĐĩҁĐģи ĐžĐŊи Ņ…Ņ€Đ°ĐŊŅŅ‚ŅŅ в iCloud", "image": "Đ˜ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ", @@ -1081,8 +1115,8 @@ "include_archived": "ĐžŅ‚ĐžĐąŅ€Đ°ĐļĐ°Ņ‚ŅŒ Đ°Ņ€Ņ…Đ¸Đ˛", "include_shared_albums": "ВĐēĐģŅŽŅ‡Đ°Ņ‚ŅŒ ĐžĐąŅ‰Đ¸Đĩ аĐģŅŒĐąĐžĐŧŅ‹", "include_shared_partner_assets": "ВĐēĐģŅŽŅ‡Đ°Ņ‚ŅŒ ĐžĐąŅ‰Đ¸Đĩ Ņ€ĐĩŅŅƒŅ€ŅŅ‹ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°", - "individual_share": "ПĐĩŅ€ŅĐžĐŊаĐģҌĐŊŅ‹Đš Đ´ĐžŅŅ‚ŅƒĐŋ", - "individual_shares": "ИĐŊĐ´Đ¸Đ˛Đ¸Đ´ŅƒĐ°ĐģҌĐŊŅ‹Đš Đ´ĐžŅŅ‚ŅƒĐŋ", + "individual_share": "ИĐŊĐ´Đ¸Đ˛Đ¸Đ´ŅƒĐ°ĐģҌĐŊĐ°Ņ ĐŋĐžĐ´ĐąĐžŅ€Đēа", + "individual_shares": "ĐŸĐžĐ´ĐąĐžŅ€Đēи", "info": "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ", "interval": { "day_at_onepm": "КаĐļĐ´Ņ‹Đš Đ´ĐĩĐŊҌ в 13:00", @@ -1103,7 +1137,7 @@ "items_count": "{count, plural, one {# ŅĐģĐĩĐŧĐĩĐŊŅ‚} many {# ŅĐģĐĩĐŧĐĩĐŊŅ‚ĐžĐ˛} other {# ŅĐģĐĩĐŧĐĩĐŊŅ‚Đ°}}", "jobs": "Đ—Đ°Đ´Đ°Ņ‡Đ¸", "keep": "ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ", - "keep_all": "ĐĄĐžŅ…Ņ€Đ°ĐŊĐ¸Ņ‚ŅŒ Đ˛ŅŅ‘", + "keep_all": "ĐĄĐžŅ…Ņ€Đ°ĐŊĐ¸Ņ‚ŅŒ Đ˛ŅĐĩ", "keep_this_delete_others": "ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ ŅŅ‚ĐžŅ‚, ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ĐžŅŅ‚Đ°ĐģҌĐŊŅ‹Đĩ", "kept_this_deleted_others": "ĐĄĐžŅ…Ņ€Đ°ĐŊŅ‘ĐŊ ŅŅ‚ĐžŅ‚ ĐžĐąŅŠĐĩĐēŅ‚ и {count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚ ŅƒĐ´Đ°ĐģŅ‘ĐŊ} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ŅƒĐ´Đ°ĐģĐĩĐŊĐž} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ° ŅƒĐ´Đ°ĐģĐĩĐŊĐž}}", "keyboard_shortcuts": "ĐĄĐžŅ‡ĐĩŅ‚Đ°ĐŊĐ¸Ņ ĐēĐģĐ°Đ˛Đ¸Ņˆ", @@ -1127,6 +1161,7 @@ "library_page_sort_created": "НĐĩдавĐŊĐž ŅĐžĐˇĐ´Đ°ĐŊĐŊŅ‹Đĩ", "library_page_sort_last_modified": "ĐŸĐžŅĐģĐĩĐ´ĐŊĐĩĐĩ иСĐŧĐĩĐŊĐĩĐŊиĐĩ", "library_page_sort_title": "НазваĐŊиĐĩ аĐģŅŒĐąĐžĐŧа", + "licenses": "Đ›Đ¸Ņ†ĐĩĐŊСии", "light": "ХвĐĩŅ‚ĐģĐ°Ņ", "like_deleted": "ЛайĐē ŅƒĐ´Đ°ĐģĐĩĐŊ", "link_motion_video": "ĐĄŅŅ‹ĐģĐēа ĐŊа двиĐļŅƒŅ‰ĐĩĐĩŅŅ видĐĩĐž", @@ -1136,7 +1171,9 @@ "list": "ĐĄĐŋĐ¸ŅĐžĐē", "loading": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа", "loading_search_results_failed": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚ĐžĐ˛ ĐŋĐžĐ¸ŅĐēа ĐŊĐĩ ŅƒĐ´Đ°ĐģĐ°ŅŅŒ", + "local": "На ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ", "local_asset_cast_failed": "НĐĩвОСĐŧĐžĐļĐŊĐž Ņ‚Ņ€Đ°ĐŊҁĐģĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐžĐąŅŠĐĩĐēŅ‚, ĐēĐžŅ‚ĐžŅ€Ņ‹Đš Đĩ҉ґ ĐŊĐĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐĩĐŊ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€", + "local_assets": "ĐžĐąŅŠĐĩĐē҂ҋ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ", "local_network": "ЛоĐēаĐģҌĐŊĐ°Ņ ҁĐĩŅ‚ŅŒ", "local_network_sheet_info": "ĐŸŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ ĐąŅƒĐ´ĐĩŅ‚ ĐŋОдĐēĐģŅŽŅ‡Đ°Ņ‚ŅŒŅŅ Đē ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ ĐŋĐž ŅŅ‚ĐžĐŧ҃ Đ°Đ´Ņ€Đĩҁ҃, ĐēĐžĐŗĐ´Đ° ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž ĐŋОдĐēĐģŅŽŅ‡ĐĩĐŊĐž Đē Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊОК Wi-Fi ҁĐĩŅ‚Đ¸", "location_permission": "Đ”ĐžŅŅ‚ŅƒĐŋ Đē ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊĐ¸ŅŽ", @@ -1246,6 +1283,7 @@ "more": "БоĐģҌ҈Đĩ", "move": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ", "move_off_locked_folder": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ иС ĐģĐ¸Ņ‡ĐŊОК ĐŋаĐŋĐēи", + "move_to_lock_folder_action_prompt": "{count} дОйавĐģĐĩĐŊĐž в Đ›Đ¸Ņ‡ĐŊŅƒŅŽ ĐŋаĐŋĐē҃", "move_to_locked_folder": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ в ĐģĐ¸Ņ‡ĐŊŅƒŅŽ ĐŋаĐŋĐē҃", "move_to_locked_folder_confirmation": "Đ­Ņ‚Đ¸ Ņ„ĐžŅ‚Đž и видĐĩĐž ĐąŅƒĐ´ŅƒŅ‚ ŅƒĐ´Đ°ĐģĐĩĐŊŅ‹ иС Đ˛ŅĐĩŅ… аĐģŅŒĐąĐžĐŧОв и ĐąŅƒĐ´ŅƒŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ‹ Ņ‚ĐžĐģҌĐēĐž в ĐģĐ¸Ņ‡ĐŊОК ĐŋаĐŋĐēĐĩ", "moved_to_archive": "{count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚ ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ґĐŊ} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊĐž} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ° ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊĐž}} в Đ°Ņ€Ņ…Đ¸Đ˛", @@ -1292,6 +1330,7 @@ "no_results": "НĐĩŅ‚ Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚ĐžĐ˛", "no_results_description": "ПоĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ Đ¸ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ ŅĐ¸ĐŊĐžĐŊиĐŧ иĐģи йОĐģĐĩĐĩ ĐžĐąŅ‰ĐĩĐĩ ĐēĐģŅŽŅ‡ĐĩвОĐĩ ҁĐģОвО", "no_shared_albums_message": "ĐĄĐžĐˇĐ´Đ°ĐšŅ‚Đĩ аĐģŅŒĐąĐžĐŧ Đ´ĐģŅ ОйĐŧĐĩĐŊа Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸ŅĐŧи и видĐĩОСаĐŋĐ¸ŅŅĐŧи ҁ ĐģŅŽĐ´ŅŒĐŧи в Đ˛Đ°ŅˆĐĩĐš ҁĐĩŅ‚Đ¸", + "no_uploads_in_progress": "НĐĩŅ‚ аĐēŅ‚Đ¸Đ˛ĐŊҋ҅ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐžĐē", "not_in_any_album": "Ни в ОдĐŊĐžĐŧ аĐģŅŒĐąĐžĐŧĐĩ", "not_selected": "НĐĩ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐž", "note_apply_storage_label_to_previously_uploaded assets": "ĐŸŅ€Đ¸ĐŧĐĩŅ‡Đ°ĐŊиĐĩ: Đ§Ņ‚ĐžĐąŅ‹ ĐŋŅ€Đ¸ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŧĐĩŅ‚Đē҃ Ņ…Ņ€Đ°ĐŊиĐģĐ¸Ņ‰Đ° Đē Ņ€Đ°ĐŊĐĩĐĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐĩĐŊĐŊŅ‹Đŧ Ņ€ĐĩŅŅƒŅ€ŅĐ°Đŧ, СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đĩ", @@ -1329,6 +1368,7 @@ "original": "ĐžŅ€Đ¸ĐŗĐ¸ĐŊаĐģ", "other": "Đ”Ņ€ŅƒĐŗĐžĐĩ", "other_devices": "Đ”Ņ€ŅƒĐŗĐ¸Đĩ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ°", + "other_entities": "Đ”Ņ€ŅƒĐŗĐ¸Đĩ ĐžĐąŅŠĐĩĐē҂ҋ", "other_variables": "Đ”Ņ€ŅƒĐŗĐ¸Đĩ ĐŋĐĩŅ€ĐĩĐŧĐĩĐŊĐŊŅ‹Đĩ", "owned": "Мои", "owner": "ВĐģадĐĩĐģĐĩ҆", @@ -1426,7 +1466,7 @@ "profile_drawer_server_out_of_date_minor": "ВĐĩŅ€ŅĐ¸Ņ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ŅƒŅŅ‚Đ°Ņ€ĐĩĐģа. ПоĐļаĐģŅƒĐšŅŅ‚Đ°, ОйĐŊĐžĐ˛Đ¸Ņ‚Đĩ ĐĩĐŗĐž.", "profile_image_of_user": "Đ˜ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊиĐĩ ĐŋŅ€ĐžŅ„Đ¸ĐģŅ {user}", "profile_picture_set": "Đ¤ĐžŅ‚Đž ĐŋŅ€ĐžŅ„Đ¸ĐģŅ ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž.", - "public_album": "ĐŸŅƒĐąĐģĐ¸Ņ‡ĐŊŅ‹Đš аĐģŅŒĐąĐžĐŧ", + "public_album": "ĐžĐąŅ‰Đ¸Đš аĐģŅŒĐąĐžĐŧ", "public_share": "ĐŸŅƒĐąĐģĐ¸Ņ‡ĐŊŅ‹Đš Đ´ĐžŅŅ‚ŅƒĐŋ", "purchase_account_info": "ПоддĐĩŅ€ĐļĐēа", "purchase_activated_subtitle": "БĐģĐ°ĐŗĐžĐ´Đ°Ņ€Đ¸Đŧ Đ˛Đ°Ņ Са ĐŋОддĐĩŅ€ĐļĐē҃ Immich и ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŧĐŊĐžĐŗĐž ОйĐĩҁĐŋĐĩ҇ĐĩĐŊĐ¸Ņ ҁ ĐžŅ‚ĐēҀҋ҂ҋĐŧ Đ¸ŅŅ…ĐžĐ´ĐŊŅ‹Đŧ ĐēОдОĐŧ", @@ -1460,6 +1500,7 @@ "purchase_server_description_2": "ĐĄĐžŅŅ‚ĐžŅĐŊиĐĩ ĐŋОддĐĩŅ€ĐļĐēи", "purchase_server_title": "ĐĄĐĩŅ€Đ˛ĐĩŅ€", "purchase_settings_server_activated": "КĐģŅŽŅ‡ĐžĐŧ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Đ° ҃ĐŋŅ€Đ°Đ˛ĐģŅĐĩŅ‚ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "queue_status": "В ĐžŅ‡ĐĩŅ€Đĩди {count}/{total}", "rating": "Đ ĐĩĐšŅ‚Đ¸ĐŊĐŗ ĐˇĐ˛Ņ‘ĐˇĐ´", "rating_clear": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", "rating_count": "{count, plural, one {# СвĐĩСда} many {# СвĐĩСд} other {# СвĐĩĐˇĐ´Ņ‹}}", @@ -1488,6 +1529,8 @@ "refreshing_faces": "ОбĐŊОвĐģĐĩĐŊиĐĩ ĐģĐ¸Ņ†", "refreshing_metadata": "ОбĐŊОвĐģĐĩĐŊиĐĩ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐŊҋ҅", "regenerating_thumbnails": "Đ’ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊиĐĩ ĐŧиĐŊĐ¸Đ°Ņ‚ŅŽŅ€", + "remote": "На ҁĐĩŅ€Đ˛ĐĩŅ€Đĩ", + "remote_assets": "ĐžĐąŅŠĐĩĐē҂ҋ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€Đĩ", "remove": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ", "remove_assets_album_confirmation": "Đ’Ņ‹ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ‚ĐĩĐģҌĐŊĐž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ {count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ°}} иС аĐģŅŒĐąĐžĐŧа?", "remove_assets_shared_link_confirmation": "Đ’Ņ‹ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ‚ĐĩĐģҌĐŊĐž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ {count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ°}} иС ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊĐžĐŗĐž Đ´ĐžŅŅ‚ŅƒĐŋа ĐŋĐž ŅŅ‚ĐžĐš ҁҁҋĐģĐēĐĩ?", @@ -1495,7 +1538,9 @@ "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": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ иС ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊОК ҁҁҋĐģĐēи", @@ -1523,19 +1568,24 @@ "reset_password": "ĐĄĐąŅ€ĐžŅ ĐŋĐ°Ņ€ĐžĐģŅ", "reset_people_visibility": "Đ’ĐžŅŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ видиĐŧĐžŅŅ‚ŅŒ ĐģŅŽĐ´ĐĩĐš", "reset_pin_code": "ĐĄĐąŅ€ĐžŅĐ¸Ņ‚ŅŒ PIN-ĐēОд", + "reset_sqlite": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ ĐąĐ°ĐˇŅƒ даĐŊĐŊҋ҅ SQLite", + "reset_sqlite_confirmation": "Đ’Ņ‹ ŅƒĐ˛ĐĩŅ€ĐĩĐŊŅ‹, Ņ‡Ņ‚Đž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ ĐąĐ°ĐˇŅƒ даĐŊĐŊҋ҅ SQLite? ВаĐŧ ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒĐĩŅ‚ŅŅ Đ˛Ņ‹ĐšŅ‚Đ¸ иС ŅĐ¸ŅŅ‚ĐĩĐŧŅ‹ и ҁĐŊОва Đ˛ĐžĐšŅ‚Đ¸ Đ´ĐģŅ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊОК ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Đ¸ даĐŊĐŊҋ҅.", + "reset_sqlite_success": "База даĐŊĐŊҋ҅ SQLite ҃ҁĐŋĐĩ҈ĐŊĐž ĐžŅ‡Đ¸Ņ‰ĐĩĐŊа", "reset_to_default": "Đ’ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊиĐĩ СĐŊĐ°Ņ‡ĐĩĐŊиК ĐŋĐž ҃ĐŧĐžĐģŅ‡Đ°ĐŊĐ¸ŅŽ", "resolve_duplicates": "ĐŖŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ‚ŅŒ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹", "resolved_all_duplicates": "Đ’ŅĐĩ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹ ŅƒŅŅ‚Ņ€Đ°ĐŊĐĩĐŊŅ‹", "restore": "Đ’ĐžŅŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ", "restore_all": "Đ’ĐžŅŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ Đ˛ŅĐĩ", + "restore_trash_action_prompt": "{count} Đ˛ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž иС ĐēĐžŅ€ĐˇĐ¸ĐŊŅ‹", "restore_user": "Đ’ĐžŅŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ", "restored_asset": "Đ’ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ‹Đš ĐžĐąŅŠĐĩĐēŅ‚", "resume": "ĐŸŅ€ĐžĐ´ĐžĐģĐļĐ¸Ņ‚ŅŒ", "retry_upload": "ĐŸĐžĐ˛Ņ‚ĐžŅ€Đ¸Ņ‚ŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐē҃", - "review_duplicates": "ĐŸĐžŅĐŧĐžŅ‚Ņ€ĐĩŅ‚ŅŒ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹", + "review_duplicates": "Đ Đ°ĐˇĐžĐąŅ€Đ°Ņ‚ŅŒ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹", "role": "Đ ĐžĐģҌ", "role_editor": "Đ ĐĩдаĐēŅ‚ĐžŅ€", "role_viewer": "Đ—Ņ€Đ¸Ņ‚ĐĩĐģҌ", + "running": "Đ’Ņ‹ĐŋĐžĐģĐŊŅĐĩŅ‚ŅŅ", "save": "ĐĄĐžŅ…Ņ€Đ°ĐŊĐ¸Ņ‚ŅŒ", "save_to_gallery": "ĐĄĐžŅ…Ņ€Đ°ĐŊĐ¸Ņ‚ŅŒ в ĐŗĐ°ĐģĐĩŅ€ĐĩŅŽ", "saved_api_key": "API ĐēĐģŅŽŅ‡ иСĐŧĐĩĐŊŅ‘ĐŊ", @@ -1607,18 +1657,18 @@ "select": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ", "select_album_cover": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ ОйĐģĐžĐļĐē҃ аĐģŅŒĐąĐžĐŧа", "select_all": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ", - "select_all_duplicates": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹", + "select_all_duplicates": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ Đ´ĐģŅ ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊĐ¸Ņ", "select_all_in": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ в {group}", "select_avatar_color": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ Ņ†Đ˛ĐĩŅ‚ Đ°Đ˛Đ°Ņ‚Đ°Ņ€Đ°", "select_face": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ ĐģĐ¸Ņ†Đž", "select_featured_photo": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ Ņ„ĐžŅ‚Đž", "select_from_computer": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ҁ ĐēĐžĐŧĐŋŅŒŅŽŅ‚ĐĩŅ€Đ°", - "select_keep_all": "ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ Đ˛ŅŅ‘ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊĐžĐĩ", + "select_keep_all": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ Đ´ĐģŅ ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊĐ¸Ņ", "select_library_owner": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ вĐģадĐĩĐģŅŒŅ†Đ° йийĐģĐ¸ĐžŅ‚ĐĩĐēи", "select_new_face": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ´Ņ€ŅƒĐŗĐžĐĩ ĐģĐ¸Ņ†Đž", "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 {Đ’Ņ‹ĐąŅ€Đ°ĐŊ # ĐžĐąŅŠĐĩĐēŅ‚} many {Đ’Ņ‹ĐąŅ€Đ°ĐŊĐž # ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛} other {Đ’Ņ‹ĐąŅ€Đ°ĐŊĐž # ĐžĐąŅŠĐĩĐēŅ‚Đ°}}", @@ -1667,6 +1717,7 @@ "settings_saved": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊŅ‹", "setup_pin_code": "ХОСдаĐŊиĐĩ PIN-ĐēОда", "share": "ПодĐĩĐģĐ¸Ņ‚ŅŒŅŅ", + "share_action_prompt": "Đ Đ°ŅŅˆĐ°Ņ€ĐĩĐŊĐž {count} ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛", "share_add_photos": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ Ņ„ĐžŅ‚Đž", "share_assets_selected": "{count} Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐž", "share_dialog_preparing": "ĐŸĐžĐ´ĐŗĐžŅ‚ĐžĐ˛Đēа...", @@ -1767,10 +1818,11 @@ "sort_recent": "НĐĩдавĐŊиĐĩ Ņ„ĐžŅ‚Đž", "sort_title": "Đ—Đ°ĐŗĐžĐģОвОĐē", "source": "Đ˜ŅŅ…ĐžĐ´ĐŊŅ‹Đš ĐēОд", - "stack": "Đ“Ņ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ", - "stack_duplicates": "Đ“Ņ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹", + "stack": "ĐĄĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ", + "stack_action_prompt": "{count} ŅĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐž", + "stack_duplicates": "ĐĄĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹", "stack_select_one_photo": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ĐŗĐģавĐŊŅƒŅŽ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸ŅŽ Đ´ĐģŅ ĐŗŅ€ŅƒĐŋĐŋŅ‹", - "stack_selected_photos": "Đ“Ņ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊŅ‹Đĩ ĐžĐąŅŠĐĩĐē҂ҋ", + "stack_selected_photos": "ĐĄĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊŅ‹Đĩ ĐžĐąŅŠĐĩĐē҂ҋ", "stacked_assets_count": "{count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚ ĐžĐąŅŠĐĩдиĐŊĐĩĐŊ} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ĐžĐąŅŠĐĩдиĐŊĐĩĐŊĐž} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ° ĐžĐąŅŠĐĩдиĐŊĐĩĐŊĐž}} в ĐŗŅ€ŅƒĐŋĐŋ҃", "stacktrace": "ĐĸŅ€Đ°ŅŅĐ¸Ņ€ĐžĐ˛Đēа ҁ҂ĐĩĐēа", "start": "ĐĄŅ‚Đ°Ņ€Ņ‚", @@ -1787,6 +1839,7 @@ "storage_quota": "ĐšĐ˛ĐžŅ‚Đ° Ņ…Ņ€Đ°ĐŊиĐģĐ¸Ņ‰Đ°", "storage_usage": "{used} иС {available}", "submit": "ĐŸĐžĐ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚ŅŒ", + "success": "ĐŖŅĐŋĐĩ҈ĐŊĐž", "suggestions": "ĐŸŅ€ĐĩĐ´ĐģĐžĐļĐĩĐŊĐ¸Ņ", "sunrise_on_the_beach": "Đ’ĐžŅŅ…ĐžĐ´ ŅĐžĐģĐŊŅ†Đ° ĐŊа ĐŋĐģŅĐļĐĩ", "support": "ПоддĐĩŅ€ĐļĐēа", @@ -1796,6 +1849,8 @@ "sync": "ХиĐŊŅ…Ņ€.", "sync_albums": "ХиĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧŅ‹", "sync_albums_manual_subtitle": "ХиĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Đ˛ŅĐĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐĩĐŊĐŊŅ‹Đĩ Ņ„ĐžŅ‚Đž и видĐĩĐž в Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊŅ‹Đĩ аĐģŅŒĐąĐžĐŧŅ‹ Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐ¸Ņ", + "sync_local": "ХиĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐģĐžĐēаĐģҌĐŊĐž", + "sync_remote": "ХиĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Ņ ҁ ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ", "sync_upload_album_setting_subtitle": "ĐĄĐžĐˇĐ´Đ°Đ˛Đ°ĐšŅ‚Đĩ и ĐˇĐ°ĐŗŅ€ŅƒĐļĐ°ĐšŅ‚Đĩ ŅĐ˛ĐžĐ¸ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đ¸ и видĐĩĐž в Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊŅ‹Đĩ аĐģŅŒĐąĐžĐŧŅ‹ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ Immich", "tag": "ĐĸĐĩĐŗ", "tag_assets": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ Ņ‚ĐĩĐŗĐ¸", @@ -1806,6 +1861,7 @@ "tag_updated": "ĐĸĐĩĐŗ {tag} иСĐŧĐĩĐŊĐĩĐŊ", "tagged_assets": "ĐĸĐĩĐŗ ĐŊаСĐŊĐ°Ņ‡ĐĩĐŊ Đ´ĐģŅ {count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚Đ°} other {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛}}", "tags": "ĐĸĐĩĐŗĐ¸", + "tap_to_run_job": "НаĐļĐŧĐ¸Ņ‚Đĩ Đ´ĐģŅ СаĐŋ҃ҁĐēа ĐˇĐ°Đ´Đ°Ņ‡Đ¸", "template": "ШайĐģĐžĐŊ", "theme": "ĐĸĐĩĐŧа", "theme_selection": "Đ’Ņ‹ĐąĐžŅ€ Ņ‚ĐĩĐŧŅ‹", @@ -1838,7 +1894,8 @@ "total": "Đ’ŅĐĩĐŗĐž", "total_usage": "ĐžĐąŅ‰Đ°Ņ ŅŅ‚Đ°Ņ‚Đ¸ŅŅ‚Đ¸Đēа", "trash": "ĐšĐžŅ€ĐˇĐ¸ĐŊа", - "trash_all": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ Đ˛ŅŅ‘", + "trash_action_prompt": "{count} ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊĐž в ĐēĐžŅ€ĐˇĐ¸ĐŊ҃", + "trash_all": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ Đ˛ŅĐĩ", "trash_count": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ {count, number}", "trash_delete_asset": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ в ĐēĐžŅ€ĐˇĐ¸ĐŊ҃", "trash_emptied": "ĐšĐžŅ€ĐˇĐ¸ĐŊа ĐžŅ‡Đ¸Ņ‰ĐĩĐŊа", @@ -1855,9 +1912,11 @@ "unable_to_change_pin_code": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ иСĐŧĐĩĐŊĐĩĐŊии PIN-ĐēОда", "unable_to_setup_pin_code": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ ŅĐžĐˇĐ´Đ°ĐŊии PIN-ĐēОда", "unarchive": "Đ’ĐžŅŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ", + "unarchive_action_prompt": "{count} ŅƒĐ´Đ°ĐģĐĩĐŊĐž иС Đ°Ņ€Ņ…Đ¸Đ˛Đ°", "unarchived_count": "{count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚ Đ˛ĐžĐˇĐ˛Ņ€Đ°Ņ‰Ņ‘ĐŊ} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ Đ˛ĐžĐˇĐ˛Ņ€Đ°Ņ‰ĐĩĐŊĐž} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ° Đ˛ĐžĐˇĐ˛Ņ€Đ°Ņ‰ĐĩĐŊĐž}} иС Đ°Ņ€Ņ…Đ¸Đ˛Đ°", "undo": "ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ", "unfavorite": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ иС Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐŗĐž", + "unfavorite_action_prompt": "{count} ŅƒĐ´Đ°ĐģĐĩĐŊĐž иС Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐŗĐž", "unhide_person": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ҇ĐĩĐģОвĐĩĐēа", "unknown": "НĐĩиСвĐĩҁ҂ĐŊĐž", "unknown_country": "НĐĩиСвĐĩҁ҂ĐŊĐ°Ņ ŅŅ‚Ņ€Đ°ĐŊа", @@ -1872,15 +1931,18 @@ "unnamed_share": "ĐžĐąŅ‰Đ¸Đš Đ´ĐžŅŅ‚ŅƒĐŋ ĐąĐĩС ĐŊаСваĐŊĐ¸Ņ", "unsaved_change": "НĐĩŅĐžŅ…Ņ€Đ°ĐŊŅ‘ĐŊĐŊĐžĐĩ иСĐŧĐĩĐŊĐĩĐŊиĐĩ", "unselect_all": "ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Đ˛Ņ‹Đ´ĐĩĐģĐĩĐŊиĐĩ", - "unselect_all_duplicates": "ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Đ˛Ņ‹ĐąĐžŅ€ Đ˛ŅĐĩŅ… Đ´ŅƒĐąĐģиĐēĐ°Ņ‚ĐžĐ˛", + "unselect_all_duplicates": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ Đ´ĐģŅ ŅƒĐ´Đ°ĐģĐĩĐŊĐ¸Ņ", "unselect_all_in": "ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Đ˛Ņ‹Đ´ĐĩĐģĐĩĐŊиĐĩ в {group}", "unstack": "Đ Đ°ĐˇĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ", + "unstack_action_prompt": "{count} Ņ€Đ°ĐˇĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐž", "unstacked_assets_count": "{count, plural, one {Đ Đ°ĐˇĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊ # ĐžĐąŅŠĐĩĐēŅ‚} many {Đ Đ°ĐˇĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐž # ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛} other {Đ Đ°ĐˇĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐž # ĐžĐąŅŠĐĩĐēŅ‚Đ°}}", + "untagged": "БĐĩС Ņ‚ĐĩĐŗĐžĐ˛", "up_next": "ĐĄĐģĐĩĐ´ŅƒŅŽŅ‰ĐĩĐĩ", "updated_at": "ОбĐŊОвĐģŅ‘ĐŊ", "updated_password": "ĐŸĐ°Ņ€ĐžĐģҌ иСĐŧĐĩĐŊŅ‘ĐŊ", "upload": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ", "upload_concurrency": "ĐŸĐ°Ņ€Đ°ĐģĐģĐĩĐģҌĐŊĐžŅŅ‚ŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēи", + "upload_details": "ĐŸĐžĐ´Ņ€ĐžĐąĐŊĐžŅŅ‚Đ¸ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēи", "upload_dialog_info": "ĐĨĐžŅ‚Đ¸Ņ‚Đĩ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊŅ‹Đĩ ĐžĐąŅŠĐĩĐē҂ҋ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€?", "upload_dialog_title": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐēŅ‚", "upload_errors": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа СавĐĩŅ€ŅˆĐĩĐŊа ҁ {count, plural, one {# ĐžŅˆĐ¸ĐąĐēОК} other {# ĐžŅˆĐ¸ĐąĐēаĐŧи}}, ОйĐŊĐžĐ˛Đ¸Ņ‚Đĩ ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Ņƒ, Ņ‡Ņ‚ĐžĐąŅ‹ ŅƒĐ˛Đ¸Đ´ĐĩŅ‚ŅŒ ĐŊĐžĐ˛Ņ‹Đĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐĩĐŊĐŊŅ‹Đĩ ĐžĐąŅŠĐĩĐē҂ҋ.", @@ -1912,6 +1974,7 @@ "user_usage_stats_description": "ĐŸĐžŅĐŧĐžŅ‚Ņ€ĐĩŅ‚ŅŒ ŅŅ‚Đ°Ņ‚Đ¸ŅŅ‚Đ¸Đē҃ Đ¸ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°ĐŊĐ¸Ņ аĐēĐēĐ°ŅƒĐŊŅ‚Đ°", "username": "ИĐŧŅ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ", "users": "ПоĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģи", + "users_added_to_album_count": "{count, plural, one {# ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģҌ дОйавĐģĐĩĐŊ} many {# ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš дОйавĐģĐĩĐŊĐž} other {# ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ дОйавĐģĐĩĐŊĐž}} Đē аĐģŅŒĐąĐžĐŧ҃", "utilities": "ĐŖŅ‚Đ¸ĐģĐ¸Ņ‚Ņ‹", "validate": "ĐŸŅ€ĐžĐ˛ĐĩŅ€Đ¸Ņ‚ŅŒ", "validate_endpoint_error": "ВвĐĩĐ´Đ¸Ņ‚Đĩ ĐēĐžŅ€Ņ€ĐĩĐēŅ‚ĐŊŅ‹Đš URL", @@ -1930,6 +1993,7 @@ "view_album": "ĐŸŅ€ĐžŅĐŧĐžŅ‚Ņ€ĐĩŅ‚ŅŒ аĐģŅŒĐąĐžĐŧ", "view_all": "ĐŸĐžŅĐŧĐžŅ‚Ņ€ĐĩŅ‚ŅŒ Đ˛ŅŅ‘", "view_all_users": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ Đ˛ŅĐĩŅ… ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš", + "view_details": "ĐŸĐžŅĐŧĐžŅ‚Ņ€ĐĩŅ‚ŅŒ ĐŋĐžĐ´Ņ€ĐžĐąĐŊĐžŅŅ‚Đ¸", "view_in_timeline": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ĐŊа Đ˛Ņ€ĐĩĐŧĐĩĐŊĐŊОК ҈ĐēаĐģĐĩ", "view_link": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ҁҁҋĐģĐē҃", "view_links": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ҁҁҋĐģĐēи", @@ -1937,7 +2001,7 @@ "view_next_asset": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ҁĐģĐĩĐ´ŅƒŅŽŅ‰Đ¸Đš ĐžĐąŅŠĐĩĐēŅ‚", "view_previous_asset": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ĐŋŅ€ĐĩĐ´Ņ‹Đ´ŅƒŅ‰Đ¸Đš ĐžĐąŅŠĐĩĐēŅ‚", "view_qr_code": "ĐŸĐžŅĐŧĐžŅ‚Ņ€ĐĩŅ‚ŅŒ QR ĐēОд", - "view_stack": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ҁ҂ĐĩĐē", + "view_stack": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ĐŗŅ€ŅƒĐŋĐŋ҃", "view_user": "ĐŸŅ€ĐžŅĐŧĐžŅ‚Ņ€ĐĩŅ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ", "viewer_remove_from_stack": "ĐŖĐąŅ€Đ°Ņ‚ŅŒ иС ĐŗŅ€ŅƒĐŋĐŋŅ‹", "viewer_stack_use_as_main_asset": "Đ˜ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ в ĐēĐ°Ņ‡ĐĩŅŅ‚Đ˛Đĩ ĐžŅĐŊОвĐŊĐžĐŗĐž ĐžĐąŅŠĐĩĐēŅ‚Đ°", diff --git a/i18n/sk.json b/i18n/sk.json index cd64c52532..a29262a6e8 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -14,6 +14,7 @@ "add_a_location": "PridaÅĨ polohu", "add_a_name": "PridaÅĨ meno", "add_a_title": "PridaÅĨ nÃĄzov", + "add_endpoint": "PridaÅĨ koncovÃŊ bod", "add_exclusion_pattern": "PridaÅĨ vzor vylÃēčenia", "add_import_path": "PridaÅĨ cestu pre import", "add_location": "PridaÅĨ polohu", @@ -33,6 +34,7 @@ "added_to_favorites_count": "PridanÊ {count, number} do obÄžÃēbenÃŊch", "admin": { "add_exclusion_pattern_description": "PridÃĄvanie vzorov na vylÃēčenie. Globovanie pomocou *, ** a ? je podporovanÊ. Ak chcete ignorovaÅĨ vÅĄetky sÃēbory v akomkoÄžvek adresÃĄri s nÃĄzvom \"Raw\", pouÅžite \"**/Raw/**\". Ak chcete ignorovaÅĨ vÅĄetky sÃēbory končiace na \".tif\", pouÅžite \"**/*.tif\". Ak chcete ignorovaÅĨ absolÃētnu cestu, pouÅžite príkaz \"/cesta/k/ignorovanym/**\".", + "admin_user": "SprÃĄvca", "asset_offline_description": "TÃĄto poloÅžka externej kniÅžnice sa uÅž na disku nenachÃĄdza a bola presunutÃĄ do koÅĄa. PokiaÄž bol sÃēbor presunutÃŊ v rÃĄmci kniÅžnice, skontrolujte časovÃē os a vyhÄžadajte novÊ odpovedajÃēce poloÅžky. Ak chcete tÃēto poloÅžku obnoviÅĨ, uistite sa, Åže je cesta k niÅžÅĄie uvedenÊmu sÃēboru prístupnÃĄ pre aplikÃĄciu Immich a prehÄžadajte kniÅžnicu.", "authentication_settings": "Overovanie a prihlÃĄsenie", "authentication_settings_description": "SpravovaÅĨ heslo, protokol OAuth a ďalÅĄie nastavenia overenia", @@ -43,26 +45,26 @@ "backup_database_enable_description": "PovoliÅĨ vÃŊpisy z databÃĄzy", "backup_keep_last_amount": "MnoÅžstvo predchÃĄdzajÃēcich vÃŊpisov, ktorÊ sa majÃē zachovaÅĨ", "backup_settings": "Nastavenia vÃŊpisu databÃĄzy", - "backup_settings_description": "SprÃĄva nastavení vÃŊpisu databÃĄzy.", + "backup_settings_description": "SpravovaÅĨ nastavenia vÃŊpisu databÃĄzy.", "cleared_jobs": "HotovÊ Ãēlohy pre: {job}", "config_set_by_file": "KonfigurÃĄcia je v sÃēčasnosti nastavenÃĄ konfiguračnÃŊm sÃēborom", "confirm_delete_library": "Naozaj chcete vymazaÅĨ kniÅžnicu {library}?", - "confirm_delete_library_assets": "Ste si istí, Åže chcete vymazaÅĨ tÃēto kniÅžnicu? Tato operÃĄcia nenÃĄvratne odstrÃĄni {count, plural, one {# contained asset} other {all # contained assets}} sÃēborov z Immich. SÃēbory budÃē ponechanÊ na disku.", + "confirm_delete_library_assets": "Ste si istí, Åže chcete vymazaÅĨ tÃēto kniÅžnicu? Tato operÃĄcia nenÃĄvratne odstrÃĄni {count, plural, one {# zahrnutÃē poloÅžku} few {# zahrnutÊ poloÅžky} other {vÅĄetkÃŊch # zahrnutÃŊch poloÅžiek}} z aplikÃĄcie Immich. SÃēbory budÃē ponechanÊ na disku.", "confirm_email_below": "Pre potvrdenie zadajte \"{email}\" niÅžÅĄie", "confirm_reprocess_all_faces": "Naozaj chcete spracovaÅĨ vÅĄetky tvÃĄre znova? Tento proces vymaÅže pomenovanÃŊch Äžudí.", - "confirm_user_password_reset": "Naozaj chcete resetovaÅĨ heslo pre {user}?", + "confirm_user_password_reset": "Naozaj chcete obnoviÅĨ heslo pre {user}?", "confirm_user_pin_code_reset": "Ste si istí, Åže chcete opätovne nastaviÅĨ PIN kÃŗd pouŞívateÄža {user}?", "create_job": "VytvoriÅĨ Ãēlohu", "cron_expression": "VÃŊraz cron", "cron_expression_description": "Nastavte interval skenovania pomocou formÃĄtu cron. Pre viac informÃĄcií navÅĄtívte Crontab Guru", - "cron_expression_presets": "Presety cron vÃŊrazov", + "cron_expression_presets": "PredvoÄžby vÃŊrazov Cron", "disable_login": "ZakÃĄzaÅĨ prihlÃĄsenie", - "duplicate_detection_job_description": "SpustiÅĨ strojovÊ učenie na poloÅžkÃĄch pre detekciu podobnÃŊch obrÃĄzkov. Spolieha sa na inteligentnÊ vyhÄžadÃĄvanie", + "duplicate_detection_job_description": "Spustite strojovÊ učenie na poloÅžkÃĄch pre detekciu podobnÃŊch obrÃĄzkov. Spolieha sa na inteligentnÊ vyhÄžadÃĄvanie", "exclusion_pattern_description": "Vylučovacie vzory VÃĄm umoŞňujÃē ignorovaÅĨ sÃēbory a priečinky pri skenovaní VaÅĄej kniÅžnice. Toto je uÅžitočnÊ, ak mÃĄte priečinky obsahujÃēce sÃēbory, ktorÊ nechcete importovaÅĨ, napríklad RAW sÃēbory.", - "external_library_management": "SprÃĄva Externej KniÅžnice", + "external_library_management": "Spravovanie externej kniÅžnice", "face_detection": "Detekcia tvÃĄrí", - "face_detection_description": "Rozpoznajte tvÃĄre v poloÅžkÃĄch pomocou strojovÊho učenia. V prípade videí sa berie do Ãēvahy len nÃĄhÄžad. „ObnoviÅĨ“ (znovu) spracuje vÅĄetky poloÅžky. „ObnoviÅĨ“ dodatočne vymaÅže vÅĄetky aktuÃĄlne Ãēdaje o tvÃĄrach. „ChÃŊbajÃēce“ zaradí do poradia poloÅžky aktív, ktorÊ eÅĄte neboli spracovanÊ. ZistenÊ tvÃĄre sa po dokončení rozpoznÃĄvania tvÃĄrí zaradia do poradia na rozpoznÃĄvanie tvÃĄrí, pričom sa zoskupia do existujÃēcich alebo novÃŊch osôb.", - "facial_recognition_job_description": "ZoskupovaÅĨ rozpoznanÊ tvÃĄre do osôb. Tento krok sa vykonÃĄ po dokončení rozpoznÃĄvania tvÃĄrí. „ObnoviÅĨ“ (znovu) zoskupí vÅĄetky tvÃĄre. „ChÃŊbajÃēce“ zaradí tvÃĄre, ktorÊ nemajÃē pridelenÃē osobu.", + "face_detection_description": "Rozpoznajte tvÃĄre v poloÅžkÃĄch pomocou strojovÊho učenia. V prípade videí sa berie do Ãēvahy len nÃĄhÄžad. „AktualizovaÅĨ“ (znovu) spracuje vÅĄetky poloÅžky. „ObnoviÅĨ“ dodatočne vymaÅže vÅĄetky aktuÃĄlne Ãēdaje o tvÃĄrach. „ChÃŊbajÃēce“ zaradí do poradia mÊdiÃĄ, ktorÊ eÅĄte neboli spracovanÊ. ZistenÊ tvÃĄre sa po dokončení rozpoznÃĄvania tvÃĄrí zaradia do poradia na rozpoznÃĄvanie tvÃĄrí, pričom sa zoskupia do existujÃēcich alebo novÃŊch osôb.", + "facial_recognition_job_description": "Zoskupte rozpoznanÊ tvÃĄre do osôb. Tento krok sa vykonÃĄ po dokončení rozpoznÃĄvania tvÃĄrí. „ObnoviÅĨ“ (znovu) zoskupí vÅĄetky tvÃĄre. „ChÃŊbajÃēce“ zaradí tvÃĄre, ktorÊ nemajÃē pridelenÃē osobu.", "failed_job_command": "Príkaz {command} zlyhal pre Ãēlohu: {job}", "force_delete_user_warning": "VAROVANIE: Toto okamÅžite odstrÃĄni pouŞívateÄža a vÅĄetky poloÅžky. Tento krok nie je moÅžnÊ vrÃĄtiÅĨ späÅĨ a sÃēbory nebude moÅžnÊ obnoviÅĨ.", "image_format": "FormÃĄt", @@ -84,7 +86,7 @@ "image_resolution_description": "VyÅĄÅĄie rozlÃ­ÅĄenie môŞe zachovaÅĨ viac detailov, ale kÃŗdovanie trvÃĄ dlhÅĄie, sÃēbory sÃē vÃ¤ÄÅĄie a môŞe to zníŞiÅĨ rÃŊchlosÅĨ odozvy aplikÃĄcie.", "image_settings": "ObrÃĄzky", "image_settings_description": "SpravovaÅĨ kvalitu a rozlÃ­ÅĄenie generovanÃŊch obrÃĄzkov", - "image_thumbnail_description": "MalÃĄ miniatÃēra s odstrÃĄnenÃŊmi metadÃĄtami, pouŞívanÊ pri zobrazovaní skupín fotiek ako na hlavnej časovej osi", + "image_thumbnail_description": "MalÃĄ miniatÃēra s odstrÃĄnenÃŊmi metadÃĄtami, ktorÃĄ sa pouŞíva pri prezeraní skupín fotografií ako na hlavnej časovej osi", "image_thumbnail_quality_description": "Kvalita miniatÃēry v stupnici od 1 do 100. VyÅĄÅĄia hodnota znamenÃĄ lepÅĄiu kvalitu, ale produkuje vÃ¤ÄÅĄie sÃēbory a môŞe zníŞiÅĨ odozvu aplikÃĄcie.", "image_thumbnail_title": "MiniatÃēry", "job_concurrency": "SÃēbeÅžnosÅĨ Ãēlohy - {job}", @@ -92,7 +94,7 @@ "job_not_concurrency_safe": "TÃĄto Ãēloha nie je bezpečnÃĄ pre sÃēbeÅžnÊ spracovanie.", "job_settings": "Úlohy", "job_settings_description": "SpravovaÅĨ sÃēbeÅžnosÅĨ Ãēloh", - "job_status": "Stav Úloh", + "job_status": "Stav Ãēloh", "jobs_delayed": "{jobCount, plural, one {# oneskorenÃŊ} few {# oneskorenÊ} other {# oneskorenÃŊch}}", "jobs_failed": "{jobCount, plural, one {# neÃēspeÅĄnÃŊ} few {# neÃēspeÅĄnÊ} other {# neÃēspeÅĄnÃŊch}}", "library_created": "VytvorenÃĄ kniÅžnica: {library}", @@ -103,18 +105,18 @@ "library_scanning_enable_description": "ZapnÃēÅĨ pravidelnÊ skenovanie kniÅžnice", "library_settings": "ExternÃĄ kniÅžnica", "library_settings_description": "SpravovaÅĨ nastavenia externej kniÅžnice", - "library_tasks_description": "VyhÄžadÃĄvanie novÃŊch alebo zmenenÃŊch poloÅžiek v externÃŊch kniÅžniciach", + "library_tasks_description": "VyhÄžadajte novÊ alebo zmenenÊ mÊdiÃĄ v externÃŊch kniÅžniciach", "library_watching_enable_description": "SledovaÅĨ externÊ kniÅžnice pre zmeny v sÃēboroch", "library_watching_settings": "Sledovanie kniÅžnice (EXPERIMENTÁLNE)", "library_watching_settings_description": "Automaticky sledovaÅĨ zmenenÊ sÃēbory", - "logging_enable_description": "PovoliÅĨ logovanie", - "logging_level_description": "Ak je povolenÊ, akÃē Ãēroveň logovania pouÅžiÅĨ.", - "logging_settings": "Logovanie", + "logging_enable_description": "PovoliÅĨ ukladanie zÃĄznamov", + "logging_level_description": "Ak je povolenÊ, akÃē Ãēroveň zÃĄznamov pouÅžiÅĨ.", + "logging_settings": "Ukladanie zÃĄznamov", "machine_learning_clip_model": "Model CLIP", "machine_learning_clip_model_description": "NÃĄzov modelu CLIP je uvedenÃŊ tu. Pamätajte, Åže pri zmene modelu je nutnÊ znovu spustiÅĨ Ãēlohu 'InteligentnÊ vyhÄžadÃĄvanie' pre vÅĄetky obrÃĄzky.", "machine_learning_duplicate_detection": "Detekcia duplikÃĄtov", "machine_learning_duplicate_detection_enabled": "PovoliÅĨ detekciu duplikÃĄtov", - "machine_learning_duplicate_detection_enabled_description": "Ak je vypnutÊ, presne identickÊ poloÅžky budÃē stÃĄle deduplikovanÊ.", + "machine_learning_duplicate_detection_enabled_description": "Ak je vypnutÊ, Ãēplne identickÊ poloÅžky budÃē stÃĄle deduplikovanÊ.", "machine_learning_duplicate_detection_setting_description": "PouÅžiÅĨ CLIP embeddings na identifikÃĄciu pravdepodobnÃŊch duplikÃĄtov", "machine_learning_enabled": "PovoliÅĨ strojovÊ učenie", "machine_learning_enabled_description": "Ak je vypnutÊ, vÅĄetky funkcie strojovÊho učenia (ML) budÃē vypnutÊ, bez ohÄžadu na nastavenia niÅžÅĄie.", @@ -124,10 +126,10 @@ "machine_learning_facial_recognition_model_description": "Modely sÃē zoradenÊ od najvÃ¤ÄÅĄieho po najmenÅĄÃ­. VÃ¤ÄÅĄie modely sÃē pomalÅĄie a vyÅžadujÃē viac pamäte, ale poskytujÃē lepÅĄie vÃŊsledky. Pamätajte, Åže po zmene modelu je potrebnÊ znovu spustiÅĨ Ãēlohu detekcie tvÃĄrí pre vÅĄetky obrÃĄzky.", "machine_learning_facial_recognition_setting": "PovoliÅĨ rozpoznÃĄvanie tvÃĄrí", "machine_learning_facial_recognition_setting_description": "Ak je vypnutÊ, obrÃĄzky nebudÃē spracovanÊ pre rozpoznÃĄvanie tvÃĄrí a nebudÃē sa zobrazovaÅĨ v sekcii ÄŊudia na strÃĄnke PreskÃēmaÅĨ.", - "machine_learning_max_detection_distance": "MaximÃĄlna detekčnÃĄ odchylka", - "machine_learning_max_detection_distance_description": "MaximÃĄlna odchylka medzi dvoma obrÃĄzkami, aby boli povaÅžovanÊ za duplikÃĄty, v rozsahu od 0.001 do 0.1. VyÅĄÅĄie hodnoty odhalia viac duplikÃĄtov, ale môŞu viesÅĨ k faloÅĄnÃŊm pozitívam.", - "machine_learning_max_recognition_distance": "MaximÃĄlna rozpoznÃĄvacia odchylka", - "machine_learning_max_recognition_distance_description": "MaximÃĄlna odchylka medzi dvoma tvÃĄrami, aby boli povaÅžovanÊ za rovnakÃē osobu, v rozsahu od 0 do 2. ZníŞenie tejto hodnoty môŞe zabrÃĄniÅĨ označeniu dvoch Äžudí za tÃē istÃē osobu, zatiaÄž čo zvÃŊÅĄenie môŞe zabrÃĄniÅĨ označeniu jednej osoby za dve rôzne osoby. Pamätajte, Åže je jednoduchÅĄie spojiÅĨ dvoch Äžudí ako rozdeliÅĨ jednu osobu na dve, takÅže je lepÅĄie voliÅĨ niÅžÅĄÃ­ prah, ak je to moÅžnÊ.", + "machine_learning_max_detection_distance": "MaximÃĄlna detekčnÃĄ odchÃŊlka", + "machine_learning_max_detection_distance_description": "MaximÃĄlna odchÃŊlka medzi dvoma obrÃĄzkami, aby boli povaÅžovanÊ za duplikÃĄty, v rozsahu od 0.001 do 0.1. VyÅĄÅĄie hodnoty odhalia viac duplikÃĄtov, ale môŞu viesÅĨ k faloÅĄnÃŊm pozitívam.", + "machine_learning_max_recognition_distance": "MaximÃĄlna rozpoznÃĄvacia odchÃŊlka", + "machine_learning_max_recognition_distance_description": "MaximÃĄlna odchÃŊlka medzi dvomi tvÃĄrami, aby boli povaÅžovanÊ za rovnakÃē osobu, v rozsahu od 0 do 2. ZníŞenie tejto hodnoty môŞe zabrÃĄniÅĨ označeniu dvoch Äžudí za tÃē istÃē osobu, zatiaÄž čo zvÃŊÅĄenie môŞe zabrÃĄniÅĨ označeniu jednej osoby za dve rôzne osoby. Pamätajte, Åže je jednoduchÅĄie spojiÅĨ dvoch Äžudí ako rozdeliÅĨ jednu osobu na dve, takÅže je lepÅĄie voliÅĨ niÅžÅĄÃ­ prah, ak je to moÅžnÊ.", "machine_learning_min_detection_score": "MinimÃĄlne detekčnÊ skÃŗre", "machine_learning_min_detection_score_description": "MinimÃĄlne skÃŗre dôveryhodnosti pre detekciu tvÃĄre v rozsahu od 0 do 1. NiÅžÅĄie hodnoty odhalia viac tvÃĄrí, ale môŞu viesÅĨ k faloÅĄnÃŊm pozitivním vÃŊsledkom.", "machine_learning_min_recognized_faces": "Minimum rozpoznanÃŊch tvÃĄrí", @@ -139,15 +141,15 @@ "machine_learning_smart_search_enabled": "PovoliÅĨ inteligentnÊ vyhÄžadÃĄvanie", "machine_learning_smart_search_enabled_description": "Ak je vypnutÊ, obrÃĄzky nebudÃē spracovanÊ pre inteligentnÊ vyhÄžadÃĄvanie.", "machine_learning_url_description": "URL adresa servera strojovÊho učenia. Ak je zadanÃŊch viacero adries URL, kaÅždÃŊ server bude testovanÃŊ postupne, kÃŊm jeden z nich neodpovie ÃēspeÅĄne, v poradí od prvÊho po poslednÃŊ. Servery, ktorÊ neodpovedajÃē, budÃē dočasne ignorovanÊ, kÃŊm nebudÃē opäÅĨ online.", - "manage_concurrency": "SprÃĄva sÃēbeÅžnosti", - "manage_log_settings": "SpravovaÅĨ nastavenia logovania", + "manage_concurrency": "SpravovaÅĨ sÃēbeÅžnosÅĨ", + "manage_log_settings": "SpravovaÅĨ nastavenia ukladania zÃĄznamov", "map_dark_style": "TmavÃŊ ÅĄtÃŊl", "map_enable_description": "PovoliÅĨ funkcie mapy", - "map_gps_settings": "Mapa & GPS", - "map_gps_settings_description": "SprÃĄva nastavení mÃĄp a GPS reverznÊho geokÃŗdovania", + "map_gps_settings": "Mapa a nastavenia GPS", + "map_gps_settings_description": "SpravovaÅĨ nastavenia mapy a GPS (reverznÊ geokÃŗdovanie)", "map_implications": "TÃĄto funkčnosÅĨ sa spolieha na externÃŊ servis spracovania mapovÃŊch dlaÅždíc (tiles.immich.cloud)", "map_light_style": "SvetlÃŊ ÅĄtÃŊl", - "map_manage_reverse_geocoding_settings": "SprÃĄva nastavení ReverznÊho geokÃŗdovania", + "map_manage_reverse_geocoding_settings": "SpravovaÅĨ nastavenia reverznÊho geokÃŗdovania", "map_reverse_geocoding": "ReverznÊ GeokÃŗdovanie", "map_reverse_geocoding_enable_description": "PovoliÅĨ reverznÊ geokÃŗdovanie", "map_reverse_geocoding_settings": "ReverznÊ geokÃŗdovanie", @@ -157,16 +159,30 @@ "memory_cleanup_job": "VymazÃĄvanie spomienok", "memory_generate_job": "VytvÃĄranie spomienok", "metadata_extraction_job": "ExtrahovaÅĨ metadÃĄta", - "metadata_extraction_job_description": "Vytiahne metadÃĄta z kaÅždej poloÅžky, ako napríklad GPS, tvÃĄre a rozlÃ­ÅĄenie", + "metadata_extraction_job_description": "Získajte informÃĄcie o metadÃĄtach z kaÅždÊho mÊdia, ako sÃē GPS, tvÃĄre a rozlÃ­ÅĄenie", "metadata_faces_import_setting": "PovoliÅĨ import tvÃĄre", - "metadata_faces_import_setting_description": "Importuj tvÃĄre z EXIF dÃĄt obrÃĄzkov a sidecar sÃēborov", + "metadata_faces_import_setting_description": "ImportovaÅĨ tvÃĄre z EXIF Ãēdajov obrÃĄzka a pridruÅženÃŊch sÃēborov", "metadata_settings": "MetadÃĄta", "metadata_settings_description": "SpravovaÅĨ nastavenia metadÃĄt", "migration_job": "MigrÃĄcia", - "migration_job_description": "MigrÃĄcia miniatÃēr poloÅžiek a tvÃĄrí na najnovÅĄiu ÅĄtruktÃēru priečinkov", + "migration_job_description": "PresunÃēÅĨ miniatÃēry pre mÊdiÃĄ a tvÃĄre do najnovÅĄej ÅĄtruktÃēry priečinkov", + "nightly_tasks_cluster_faces_setting_description": "SpustiÅĨ rozpoznÃĄvanie tvÃĄre na novo-zistenÃŊch tvÃĄrach", + "nightly_tasks_cluster_new_faces_setting": "ZoskupiÅĨ novÊ tvÃĄre", + "nightly_tasks_database_cleanup_setting": "Úlohy čistenia databÃĄzy", + "nightly_tasks_database_cleanup_setting_description": "VyčistiÅĨ databÃĄzu od starÃŊch, neplatnÃŊch Ãēdajov", + "nightly_tasks_generate_memories_setting": "VytvoriÅĨ spomienky", + "nightly_tasks_generate_memories_setting_description": "VytvoriÅĨ novÊ spomienky z poloÅžiek", + "nightly_tasks_missing_thumbnails_setting": "VytvoriÅĨ chÃŊbajÃēce nÃĄhÄžady", + "nightly_tasks_missing_thumbnails_setting_description": "ZaradiÅĨ poloÅžky bez nÃĄhÄžadov do poradia na vytvorenie nÃĄhÄžadov", + "nightly_tasks_settings": "Nastavenia nočnÃŊch Ãēloh", + "nightly_tasks_settings_description": "SpravovaÅĨ nočnÊ Ãēlohy", + "nightly_tasks_start_time_setting": "Čas spustenia", + "nightly_tasks_start_time_setting_description": "Čas, kedy server začne vykonÃĄvaÅĨ nočnÊ Ãēlohy", + "nightly_tasks_sync_quota_usage_setting": "SynchronizovaÅĨ vyuÅžitie kvÃŗty", + "nightly_tasks_sync_quota_usage_setting_description": "AktualizovaÅĨ kvÃŗtu ÃēloÅžiska pouŞívateÄža na zÃĄklade aktuÃĄlneho vyuÅžitia", "no_paths_added": "Neboli pridanÊ Åžiadne cesty", "no_pattern_added": "Nebol pridanÃŊ Åžiadny vzor", - "note_apply_storage_label_previous_assets": "PoznÃĄmka: Ak chcete pouÅžiÅĨ Å títkovanie ÃēloÅžiska na predtÃŊm nahranÊ aktíva, spustite príkaz", + "note_apply_storage_label_previous_assets": "PoznÃĄmka: Ak chcete pouÅžiÅĨ ÅĄtítok ÃēloÅžiska na predtÃŊm nahranÊ poloÅžky, spustite príkaz", "note_cannot_be_changed_later": "POZNÁMKA: Toto nie je moÅžnÊ neskôr zmeniÅĨ!", "notification_email_from_address": "Z adresy", "notification_email_from_address_description": "E-mailovÃĄ adresa odosielateÄža, napríklad: \"Immich Foto Server \". Uistite sa, Åže pouŞívate adresu, z ktorej mÃĄte povolenÊ odosielaÅĨ e-maily.", @@ -189,19 +205,24 @@ "oauth_auto_register": "AutomatickÃĄ regristrÃĄcia", "oauth_auto_register_description": "AutomatickÊ zaregistrovanie novÊho poŞívateÄža pri prihlÃĄsení pomocou OAuth", "oauth_button_text": "Text tlačítka", + "oauth_client_secret_description": "VyÅžaduje sa, ak poskytovateÄž OAuth nepodporuje PKCE (Proof Key for Code Exchange)", "oauth_enable_description": "PrihlÃĄsiÅĨ sa pomocou OAuth", "oauth_mobile_redirect_uri": "URI mobilnÊho presmerovania", "oauth_mobile_redirect_uri_override": "Prepísanie URI mobilnÊho presmerovania", "oauth_mobile_redirect_uri_override_description": "PovoÄžte, keď poskytovateÄž protokolu OAuth nepovoÄžuje identifikÃĄtor URI pre mobilnÊ zariadenia, napríklad ''{callback}''", + "oauth_role_claim": "PoÅžiadavka na rolu", + "oauth_role_claim_description": "Automaticky udeliÅĨ prístup sprÃĄvcu na zÃĄklade prítomnosti tejto poÅžiadavky. PoÅžiadavka môŞe maÅĨ príznak „user“ alebo „admin“.", "oauth_settings": "OAuth", "oauth_settings_description": "SpravovaÅĨ nastavenia prihlÃĄsenia OAuth", - "oauth_settings_more_details": "Pre viac informÃĄcii o tejto funkcii, prejdite na docs.", + "oauth_settings_more_details": "Pre viac informÃĄcii o tejto funkcii, prejdite na dokumentÃĄciu.", "oauth_storage_label_claim": "NÃĄrokovaÅĨ Å títok ÃēloÅžiska", - "oauth_storage_label_claim_description": "Automaticky nastaviÅĨ Å títok ÃēloÅžiska pouŞívateÄža na hodnotu tohto nÃĄroku.", + "oauth_storage_label_claim_description": "Automaticky nastaviÅĨ ÅĄtítok ÃēloÅžiska pouŞívateÄža na hodnotu tohto nÃĄroku.", "oauth_storage_quota_claim": "DeklarÃĄcia kvÃŗty ÃēloÅžiska", "oauth_storage_quota_claim_description": "Automaticky nastaviÅĨ kvÃŗtu ÃēloÅžiska pouŞívateÄža na hodnotu tejto deklarÃĄcie.", "oauth_storage_quota_default": "PredvolenÃŊ limit ÃēloÅžiska (GiB)", "oauth_storage_quota_default_description": "KvÃŗta v GiB, ktorÃĄ sa pouÅžije, ak nie je poskytnutÃĄ Åžiadna poÅžiadavka.", + "oauth_timeout": "ČasovÃŊ limit poÅžiadavky", + "oauth_timeout_description": "ČasovÃŊ limit pre poÅžiadavky v milisekundÃĄch", "password_enable_description": "PrihlÃĄsiÅĨ sa pomocou emailu a hesla", "password_settings": "PrihlÃĄsenie cez heslo", "password_settings_description": "SpravovaÅĨ nastavenia prihlÃĄsenia cez heslo", @@ -213,7 +234,7 @@ "registration_description": "KeďŞe ste prvÃŊm pouŞívateÄžom v systÊme, budÃē vÃĄm pridelenÊ sprÃĄvcovskÊ prÃĄva na vykonÃĄvanie vÅĄetkÃŊch Ãēloh a vrÃĄtane tvorby novÃŊch pouŞívateÄžov.", "require_password_change_on_login": "VyÅžadovaÅĨ od pouŞívateÄža zmenu hesla pri prvom prihlÃĄsení", "reset_settings_to_default": "ObnoviÅĨ pôvodnÊ nastavenia", - "reset_settings_to_recent_saved": "ObnoviÅĨ naposledy uloÅženÊ nastavenia", + "reset_settings_to_recent_saved": "Nastavenia boli obnovenÊ na poslednÊ uloÅženÊ nastavenia", "scanning_library": "KniÅžnica sa skenuje", "search_jobs": "VyhÄžadaÅĨ Ãēlohyâ€Ļ", "send_welcome_email": "OdoslaÅĨ uvítací e-mail", @@ -225,8 +246,8 @@ "server_settings_description": "SpravovaÅĨ nastavenia servera", "server_welcome_message": "Uvítacia sprÃĄva", "server_welcome_message_description": "SprÃĄva, ktorÃĄ sa zobrazí na prihlasovacej strÃĄnke.", - "sidecar_job": "Sidecar metadÃĄta", - "sidecar_job_description": "Objavte alebo synchronizujte metadÃĄta Sidecar zo sÃēborovÊho systÊmu", + "sidecar_job": "PridruÅženÊ metadÃĄta", + "sidecar_job_description": "Objavte alebo synchronizujte pridruÅženÊ metadÃĄta zo sÃēborovÊho systÊmu", "slideshow_duration_description": "Čas zobrazenia obrÃĄzku v sekundÃĄch", "smart_search_job_description": "Spustite strojovÊ učenie na mÊdiÃĄch na podporu inteligentnÊho vyhÄžadÃĄvania", "storage_template_date_time_description": "ČasovÃĄ pečiatka vytvorenia poloÅžky sa pouŞíva pre informÃĄcie o dÃĄtume a čase", @@ -238,14 +259,15 @@ "storage_template_migration_description": "PouÅžite aktuÃĄlnu {template} na predtÃŊm nahranÊ mÊdiÃĄ", "storage_template_migration_info": "Å ablÃŗna ÃēloÅžiska skonvertuje vÅĄetky prípony na malÊ písmenÃĄ. Zmeny ÅĄablÃŗn sa budÃē vzÅĨahovaÅĨ iba na novÊ diela. Ak chcete ÅĄablÃŗnu spätne pouÅžiÅĨ na predtÃŊm nahranÊ mÊdiÃĄ, spustite {job}.", "storage_template_migration_job": "Úloha migrÃĄcie ÅĄablÃŗny ÃēloÅžiska", - "storage_template_more_details": "ĎalÅĄie podrobnosti o tejto funkcii nÃĄjdete v Å ablÃŗna ÃēloÅžiska a jej dôsledky", + "storage_template_more_details": "PodrobnejÅĄie informÃĄcie o tejto funkcii nÃĄjdete v časti ÅĄablÃŗna ÃēloÅžiska a jej nÃĄsledky", + "storage_template_onboarding_description_v2": "Ak je tÃĄto funkcia zapnutÃĄ, automaticky usporiada sÃēbory na zÃĄklade ÅĄablÃŗny definovanej pouŞívateÄžom. ĎalÅĄie informÃĄcie nÃĄjdete v dokumentÃĄcii.", "storage_template_path_length": "PribliÅžnÃŊ limit dÄēÅžky cesty: {length, number}/{limit, number}", "storage_template_settings": "Å ablÃŗna ÃēloÅžiska", "storage_template_settings_description": "Spravujte ÅĄtruktÃēru priečinkov a nÃĄzov sÃēboru odovzdanÊho mÊdia", "storage_template_user_label": "{label} je Å títok ÃēloÅžiska pouŞívateÄža", "system_settings": "Nastavenia systÊmu", - "tag_cleanup_job": "Premazanie značiek", - "template_email_available_tags": "V ÅĄablÃŗne môŞeÅĄ pouÅžiÅĨ nasledujÃēce stítky: {tags}", + "tag_cleanup_job": "Prečistenie ÅĄtítkov", + "template_email_available_tags": "V ÅĄablÃŗne môŞete pouÅžiÅĨ nasledujÃēce premennÊ: {tags}", "template_email_if_empty": "Ak nie je zadanÃĄ Åžiadna ÅĄablÃŗna, bude pouÅžitÃĄ predvolenÃĄ ÅĄablÃŗna.", "template_email_invite_album": "Å ablÃŗna PozvÃĄnky do albumu", "template_email_preview": "UkÃĄÅžka", @@ -256,32 +278,32 @@ "template_settings_description": "Spravovanie vlastnÃŊch ÅĄablÃŗn upozornení", "theme_custom_css_settings": "VlastnÊ CSS", "theme_custom_css_settings_description": "CSS ÅĄtÃŊly umoŞňujÃē prispôsobiÅĨ dizajn Immich.", - "theme_settings": "Motívy", + "theme_settings": "Nastavenia tÊmy", "theme_settings_description": "SpravovaÅĨ prispôsobenie webovÊho rozhrania Immich", - "thumbnail_generation_job": "GenerovaÅĨ MiniatÃēry", - "thumbnail_generation_job_description": "Generuje veÄžkÊ, malÊ a rozostrení miniatÃēry pre kaÅždÃē poloÅžku, ako aj miniatÃēry pre kaÅždÃē osobu", + "thumbnail_generation_job": "GenerovaÅĨ miniatÃēry", + "thumbnail_generation_job_description": "Generujte veÄžkÊ, malÊ a rozmazanÊ miniatÃēry pre kaÅždÃē poloÅžku, ako aj miniatÃēry pre kaÅždÃē osobu", "transcoding_acceleration_api": "API pre akcelerÃĄciu", - "transcoding_acceleration_api_description": "Rozhranie API, ktorÊ bude interagovaÅĨ s vaÅĄÃ­m zariadením s cieÄžom urÃŊchliÅĨ prekÃŗdovanie. Toto nastavenie je „najlepÅĄie Ãēsilie“: pri zlyhaní sa vrÃĄti k softvÊrovÊmu prekÃŗdovaniu. VP9 môŞe alebo nemusí fungovaÅĨ v zÃĄvislosti od vÃĄÅĄho hardvÊru.", + "transcoding_acceleration_api_description": "Rozhranie API, ktorÊ bude spolupracovaÅĨ s vaÅĄÃ­m zariadením s cieÄžom urÃŊchliÅĨ prekÃŗdovanie. Toto nastavenie je „najlepÅĄie Ãēsilie“: pri zlyhaní sa vrÃĄti k softvÊrovÊmu prekÃŗdovaniu. VP9 môŞe alebo nemusí fungovaÅĨ v zÃĄvislosti od vÃĄÅĄho hardvÊru.", "transcoding_acceleration_nvenc": "NVENC (vyÅžaduje NVIDIA GPU)", "transcoding_acceleration_qsv": "Quick Sync (vyÅžaduje 7. generÃĄciu Intel CPU alebo novÅĄiu)", "transcoding_acceleration_rkmpp": "RKMPP (iba na Rockchip SOC)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "AkceptovanÊ zvukovÊ kodeky", - "transcoding_accepted_audio_codecs_description": "Vyberte, ktorÊ zvukovÊ kodeky nie je potrebnÊ prekÃŗdovaÅĨ. PouŞíva sa len pre určitÊ zÃĄsady prekÃŗdovania.", + "transcoding_accepted_audio_codecs_description": "Vyberte, ktorÊ zvukovÊ kodeky nie je potrebnÊ prekÃŗdovaÅĨ. PouŞíva sa len pre určitÊ pravidlÃĄ prekÃŗdovania.", "transcoding_accepted_containers": "AkceptovanÊ kontajnery", - "transcoding_accepted_containers_description": "Vyberte, ktorÊ formÃĄty kontajnerov nie je potrebnÊ remuxovaÅĨ na MP4. PouŞíva sa len pre určitÊ zÃĄsady prekÃŗdovania.", + "transcoding_accepted_containers_description": "Vyberte, ktorÊ formÃĄty kontajnerov nie je potrebnÊ remuxovaÅĨ na MP4. PouŞíva sa len pre určitÊ pravidlÃĄ prekÃŗdovania.", "transcoding_accepted_video_codecs": "AkceptovanÊ video kodeky", - "transcoding_accepted_video_codecs_description": "Vyberte, ktorÊ video kodeky nie je potrebnÊ prekÃŗdovaÅĨ. PouŞíva sa len pre určitÊ zÃĄsady prekÃŗdovania.", + "transcoding_accepted_video_codecs_description": "Vyberte, ktorÊ video kodeky nie je potrebnÊ prekÃŗdovaÅĨ. PouŞíva sa len pre určitÊ pravidlÃĄ prekÃŗdovania.", "transcoding_advanced_options_description": "MoÅžnosti, ktorÊ by vÃ¤ÄÅĄina pouŞívateÄžov nemala meniÅĨ", "transcoding_audio_codec": "ZvukovÃŊ kodek", "transcoding_audio_codec_description": "Opus je najkvalitnejÅĄia moÅžnosÅĨ, ale mÃĄ niÅžÅĄiu kompatibilitu so starÃŊmi zariadeniami alebo softvÊrom.", "transcoding_bitrate_description": "VideÃĄ presahujÃēce maximÃĄlnu bitovÃē rÃŊchlosÅĨ alebo videÃĄ, ktorÊ nie sÃē v akceptovanom formÃĄte", "transcoding_codecs_learn_more": "Ak sa chcete dozvedieÅĨ viac o tu pouÅžitej terminolÃŗgii, pozrite si dokumentÃĄciu FFmpeg pre kodek H.264, kodek HEVC a VP9 kodek.", "transcoding_constant_quality_mode": "ReÅžim konÅĄtantnej kvality", - "transcoding_constant_quality_mode_description": "ICQ je lepÅĄie ako CQP, ale niektorÊ zariadenia na hardvÊrovÃē akcelerÃĄciu tento reÅžim nepodporujÃē. Nastavenie tejto moÅžnosti uprednostní ÅĄpecifikovanÃŊ reÅžim pri pouÅžití kÃŗdovania zaloÅženÊho na kvalite. IgnorovanÊ spoločnosÅĨou NVENC, pretoÅže nepodporuje ICQ.", + "transcoding_constant_quality_mode_description": "ICQ je lepÅĄie ako CQP, ale niektorÊ zariadenia na hardvÊrovÃē akcelerÃĄciu tento reÅžim nepodporujÃē. Nastavenie tejto moÅžnosti uprednostní ÅĄpecifikovanÃŊ reÅžim pri pouÅžití kÃŗdovania zaloÅženÊho na kvalite. IgnorovanÊ funkciou NVENC, pretoÅže nepodporuje ICQ.", "transcoding_constant_rate_factor": "Faktor konÅĄtantnej rÃŊchlosti (-crf)", "transcoding_constant_rate_factor_description": "Úroveň kvality videa. TypickÊ hodnoty sÃē 23 pre H.264, 28 pre HEVC, 31 pre VP9 a 35 pre AV1. NiÅžÅĄie je lepÅĄie, ale vytvÃĄra vÃ¤ÄÅĄie sÃēbory.", - "transcoding_disabled_description": "NeprekÃŗdujte Åžiadne videÃĄ, na niektorÃŊch klientoch môŞe preruÅĄiÅĨ prehrÃĄvanie", + "transcoding_disabled_description": "NeprekÃŗdovaÅĨ Åžiadne videÃĄ, na niektorÃŊch klientoch môŞe preruÅĄiÅĨ prehrÃĄvanie", "transcoding_encoding_options": "MoÅžnosti kÃŗdovania", "transcoding_encoding_options_description": "Nastavte kodeky, rozlÃ­ÅĄenie, kvalitu a ďalÅĄie moÅžnosti pre kÃŗdovanÊ videÃĄ", "transcoding_hardware_acceleration": "HardvÊrovÃĄ akcelerÃĄcia", @@ -295,17 +317,17 @@ "transcoding_max_keyframe_interval": "MaximÃĄlny interval medzi kÄžÃēčovÃŊmi snímkami", "transcoding_max_keyframe_interval_description": "Nastavuje maximÃĄlnu vzdialenosÅĨ medzi kÄžÃēčovÃŊmi snímkami. NiÅžÅĄie hodnoty zhorÅĄujÃē ÃēčinnosÅĨ kompresie, ale zlepÅĄujÃē časy vyhÄžadÃĄvania a môŞu zlepÅĄiÅĨ kvalitu v scÊnach s rÃŊchlym pohybom. Hodnota 0 nastavuje tÃēto hodnotu automaticky.", "transcoding_optimal_description": "VideÃĄ s vyÅĄÅĄÃ­m ako cieÄžovÃŊm rozlÃ­ÅĄením alebo videÃĄ, ktorÊ nie sÃē v prijateÄžnom formÃĄte", - "transcoding_policy": "Politika prekÃŗdovania", + "transcoding_policy": "PravidlÃĄ prekÃŗdovania", "transcoding_policy_description": "Nastavte, kedy bude video prekÃŗdovanÊ", "transcoding_preferred_hardware_device": "UprednostňovanÊ hardvÊrovÊ zariadenie", "transcoding_preferred_hardware_device_description": "Platí len pre VAAPI a QSV. Nastavuje uzol dri, ktorÃŊ sa pouŞíva na hardvÊrovÊ prekÃŗdovanie.", - "transcoding_preset_preset": "Prednastavenie (-preset)", + "transcoding_preset_preset": "PredvoÄžba (-preset)", "transcoding_preset_preset_description": "RÃŊchlosÅĨ kompresie. PomalÅĄie predvoÄžby vytvÃĄrajÃē menÅĄie sÃēbory a zvyÅĄujÃē kvalitu, keď sa zameriavajÃē na určitÃŊ dÃĄtovÃŊ tok. VP9 ignoruje rÃŊchlosti vyÅĄÅĄie ako „rÃŊchlejÅĄie“.", "transcoding_reference_frames": "ReferenčnÊ snímky", "transcoding_reference_frames_description": "Počet snímok, na ktorÊ sa mÃĄ odkazovaÅĨ pri kompresii danÊho snímku. VyÅĄÅĄie hodnoty zvyÅĄujÃē ÃēčinnosÅĨ kompresie, ale spomaÄžujÃē kÃŗdovanie. Hodnota 0 sa nastavuje automaticky.", "transcoding_required_description": "Iba videÃĄ, ktorÊ nie sÃē v prijatom formÃĄte", - "transcoding_settings": "TranskÃŗdovania videa", - "transcoding_settings_description": "Spravujte, ktorÊ videÃĄ sa majÃē prekÃŗdovaÅĨ a ako ich spracovaÅĨ", + "transcoding_settings": "Nastavenia prekÃŗdovania videa", + "transcoding_settings_description": "SpravovaÅĨ, ktorÊ videÃĄ sa majÃē prekÃŗdovaÅĨ a ako sa majÃē spracovaÅĨ", "transcoding_target_resolution": "CieÄžovÊ rozlÃ­ÅĄenie", "transcoding_target_resolution_description": "VyÅĄÅĄie rozlÃ­ÅĄenia môŞu zachovaÅĨ viac detailov, ale ich kÃŗdovanie trvÃĄ dlhÅĄie, majÃē vÃ¤ÄÅĄiu veÄžkosÅĨ sÃēborov a môŞu zníŞiÅĨ odozvu aplikÃĄcie.", "transcoding_temporal_aq": "ČasovÊ AQ", @@ -314,26 +336,26 @@ "transcoding_threads_description": "VyÅĄÅĄie hodnoty vedÃē k rÃŊchlejÅĄiemu kÃŗdovaniu, ale ponechÃĄvajÃē serveru menej priestoru na spracovanie inÃŊch Ãēloh počas aktivity. TÃĄto hodnota by nemala byÅĨ vÃ¤ÄÅĄia ako počet jadier CPU. Maximalizuje vyuÅžitie, ak je nastavenÃĄ na hodnotu 0.", "transcoding_tone_mapping": "TÃŗnovÊ mapovanie", "transcoding_tone_mapping_description": "SnaŞí sa zachovaÅĨ vzhÄžad videí HDR pri konverzii na SDR. KaÅždÃŊ algoritmus robí rôzne kompromisy v oblasti farieb, detailov a jasu. Hable zachovÃĄva detaily, Mobius zachovÃĄva farby a Reinhard zachovÃĄva jas.", - "transcoding_transcode_policy": "Politika prekÃŗdovania", - "transcoding_transcode_policy_description": "ZÃĄsady, kedy sa mÃĄ video prekÃŗdovaÅĨ. VideÃĄ HDR sa vÅždy prekÃŗdujÃē (okrem prípadov, keď je prekÃŗdovanie vypnutÊ).", + "transcoding_transcode_policy": "PravidlÃĄ prekÃŗdovania", + "transcoding_transcode_policy_description": "PravidlÃĄ, kedy sa mÃĄ video prekÃŗdovaÅĨ. HDR videÃĄ sa prekÃŗdujÃē vÅždy (okrem prípadov, keď je prekÃŗdovanie vypnutÊ).", "transcoding_two_pass_encoding": "DvojpriechodovÊ kÃŗdovanie", - "transcoding_two_pass_encoding_setting_description": "Prekladajte v dvoch priechodoch, aby ste vytvorili lepÅĄie zakÃŗdovanÊ videÃĄ. Keď je povolenÃŊ maximÃĄlny dÃĄtovÃŊ tok (vyÅžaduje sa na prÃĄcu s formÃĄtmi H.264 a HEVC), tento reÅžim pouŞíva rozsah dÃĄtovÊho toku na zÃĄklade maximÃĄlneho dÃĄtovÊho toku a ignoruje CRF. V prípade VP9 sa CRF môŞe pouÅžiÅĨ, ak je max bitrate vypnutÃŊ.", + "transcoding_two_pass_encoding_setting_description": "PrekÃŗdovaÅĨ v dvoch fÃĄzach, aby sa vytvorili lepÅĄie kÃŗdovanÊ videÃĄ. Keď je povolenÃŊ maximÃĄlny dÃĄtovÃŊ tok (vyÅžaduje sa na prÃĄcu s formÃĄtmi H.264 a HEVC), tento reÅžim pouŞíva rozsah dÃĄtovÊho toku na zÃĄklade maximÃĄlneho dÃĄtovÊho toku a ignoruje CRF. V prípade VP9 sa CRF môŞe pouÅžiÅĨ, ak je maximÃĄlny bitrate vypnutÃŊ.", "transcoding_video_codec": "Video kodek", - "transcoding_video_codec_description": "VP9 mÃĄ vysokÃē ÃēčinnosÅĨ a kompatibilitu s webom, ale prekÃŗdovanie trvÃĄ dlhÅĄie. HEVC mÃĄ podobnÃē vÃŊkonnosÅĨ, ale niÅžÅĄiu kompatibilitu s webom. H.264 je ÅĄiroko kompatibilnÃŊ a rÃŊchlo sa prekÃŗdovÃĄva, ale vytvÃĄra oveÄža vÃ¤ÄÅĄie sÃēbory. AV1 je najÃēčinnejÅĄÃ­ kodek, ale chÃŊba mu podpora v starÅĄÃ­ch zariadeniach.", + "transcoding_video_codec_description": "VP9 mÃĄ vysokÃē ÃēčinnosÅĨ a kompatibilitu s webom, ale prekÃŗdovanie trvÃĄ dlhÅĄie. HEVC mÃĄ podobnÃē vÃŊkonnosÅĨ, ale niÅžÅĄiu kompatibilitu s webom. H.264 je ÅĄiroko kompatibilnÃŊ a rÃŊchlo sa prekÃŗduje, ale vytvÃĄra oveÄža vÃ¤ÄÅĄie sÃēbory. AV1 je najÃēčinnejÅĄÃ­ kodek, ale chÃŊba mu podpora v starÅĄÃ­ch zariadeniach.", "trash_enabled_description": "PovoliÅĨ funkcie koÅĄa", "trash_number_of_days": "Počet dní", - "trash_number_of_days_description": "Počet dní, počas ktorÃŊch sa mÃĄ majetok ponechaÅĨ v koÅĄi pred jeho trvalÃŊm odstrÃĄnením", + "trash_number_of_days_description": "Počet dní, počas ktorÃŊch sa majÃē mÊdiÃĄ ponechaÅĨ v koÅĄi pred ich trvalÃŊm odstrÃĄnením", "trash_settings": "KÃ´ÅĄ", "trash_settings_description": "SpravovaÅĨ nastavenia koÅĄa", "user_cleanup_job": "Premazanie pouŞívateÄžov", - "user_delete_delay": "Konto {user} a jeho mÊdiÃĄ budÃē podÄža plÃĄnu natrvalo vymazanÊ za {delay, plural, one {# day} other {# days}}.", - "user_delete_delay_settings": "OdstrÃĄniÅĨ oneskorenie", - "user_delete_delay_settings_description": "Počet dní po odstrÃĄnení na trvalÊ vymazanie Ãēčtu a aktív pouŞívateÄža. Úloha odstraňovania pouŞívateÄžov sa spÃēÅĄÅĨa o polnoci, aby sa skontrolovali pouŞívatelia, ktorí sÃē pripravení na odstrÃĄnenie. Zmeny tohto nastavenia sa vyhodnotia pri ďalÅĄom spustení.", - "user_delete_immediately": "Konto a mÊdiÃĄ {user} budÃē zaradenÊ do frontu na trvalÊ vymazanie okamÅžite.", + "user_delete_delay": "Konto {user} a jeho mÊdiÃĄ budÃē podÄža plÃĄnu natrvalo vymazanÊ za {delay, plural, one {# deň} few {# dni} other {# dní}}.", + "user_delete_delay_settings": "Oneskorenie vymazania", + "user_delete_delay_settings_description": "Počet dní, po ktorÃŊch sa po odstrÃĄnení pouŞívateÄža natrvalo odstrÃĄni jeho Ãēčet a poloÅžky. Úloha odstraňovania pouŞívateÄžov sa spÃēÅĄÅĨa o polnoci, aby sa skontrolovali pouŞívatelia, ktorí sÃē pripravení na odstrÃĄnenie. Zmeny tohto nastavenia sa vyhodnotia pri ďalÅĄom spustení.", + "user_delete_immediately": "Konto a mÊdiÃĄ pouŞívateÄža {user} budÃē zaradenÊ do poradia na trvalÊ vymazanie okamÅžite.", "user_delete_immediately_checkbox": "PouŞívateÄž a mÊdiÃĄ budÃē zaradení do frontu na okamÅžitÊ vymazanie", "user_details": "Podrobnosti o pouŞívateÄžovi", - "user_management": "SprÃĄva pouŞívateÄžov", - "user_password_has_been_reset": "Heslo pouŞívateÄža bolo resetovanÊ:", + "user_management": "Spravovanie pouŞívateÄžov", + "user_password_has_been_reset": "Heslo pouŞívateÄža bolo obnovenÊ:", "user_password_reset_description": "Poskytnite pouŞívateÄžovi dočasnÊ heslo a informujte ho, Åže si ho bude musieÅĨ zmeniÅĨ pri ďalÅĄom prihlÃĄsení.", "user_restore_description": "{user} bude Ãēčet obnovenÃŊ.", "user_restore_scheduled_removal": "ObnoviÅĨ pouŞívateÄža - plÃĄnovanÊ odstrÃĄnenie na {date, date, long}", @@ -351,11 +373,19 @@ "admin_password": "AdministrÃĄtorskÊ heslo", "administration": "AdministrÃĄcia", "advanced": "PokročilÊ", - "advanced_settings_log_level_title": "Úroveň logovania: {level}", - "advanced_settings_prefer_remote_subtitle": "NiektorÊ zariadenia sÃē extrÊmne pomalÊ pre načítavanie miniatÃēr z fotiek na zariadení. PovoÄžte toto nastavenie aby sa namiesto toho načítavali obrÃĄzky zo servera.", - "advanced_settings_prefer_remote_title": "PreferovaÅĨ vzdialenÊ obrÃĄzky", + "advanced_settings_beta_timeline_subtitle": "VyskÃēÅĄajte prostredie novej aplikÃĄcie", + "advanced_settings_beta_timeline_title": "Beta verzia časovej osi", + "advanced_settings_enable_alternate_media_filter_subtitle": "TÃēto moÅžnosÅĨ pouÅžite na filtrovanie mÊdií počas synchronizÃĄcie na zÃĄklade alternatívnych kritÊrií. TÃēto moÅžnosÅĨ vyskÃēÅĄajte len vtedy, ak mÃĄte problÊmy s detekciou vÅĄetkÃŊch albumov v aplikÃĄcii.", + "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTÁLNE] PouÅžiÅĨ alternatívny filter synchronizÃĄcie albumu zariadenia", + "advanced_settings_log_level_title": "Úroveň ukladania zÃĄznamov: {level}", + "advanced_settings_prefer_remote_subtitle": "V niektorÃŊch zariadeniach sa miniatÃēry z miestnych poloÅžiek načítavajÃē veÄžmi pomaly. Aktivovaním tohto nastavenia sa namiesto toho načítajÃē vzdialenÊ obrÃĄzky.", + "advanced_settings_prefer_remote_title": "UprednostniÅĨ vzdialenÊ obrÃĄzky", + "advanced_settings_proxy_headers_subtitle": "Určite hlavičky proxy servera, ktorÊ by mal Immich posielaÅĨ s kaÅždou poÅžiadavkou na sieÅĨ", + "advanced_settings_proxy_headers_title": "Proxy hlavičky", "advanced_settings_self_signed_ssl_subtitle": "Preskakuje overovanie SSL certifikÃĄtom zo strany servera. VyÅžaduje sa pre samo-podpísanÊ certifikÃĄty.", "advanced_settings_self_signed_ssl_title": "PovoliÅĨ samo-podpísanÊ SSL certifikÃĄty", + "advanced_settings_sync_remote_deletions_subtitle": "Automaticky vymazaÅĨ alebo obnoviÅĨ poloÅžku na tomto zariadení, keď sa tÃĄto akcia vykonÃĄ na webe", + "advanced_settings_sync_remote_deletions_title": "SynchronizovaÅĨ vzdialenÊ vymazania [EXPERIMENTÁLNE]", "advanced_settings_tile_subtitle": "PokročilÊ nastavenia pouŞívateÄža", "advanced_settings_troubleshooting_subtitle": "PovoliÅĨ ďalÅĄie funkcie pre opravu chÃŊb", "advanced_settings_troubleshooting_title": "Oprava chÃŊb", @@ -363,7 +393,7 @@ "age_year_months": "Vek 1 rok, {months, plural, one {# month} other {# months}}", "age_years": "{years, plural, other {Vek #}}", "album_added": "Album bol pridanÃŊ", - "album_added_notification_setting_description": "ObdrÅžaÅĨ upozornenie emailom, keď ste pridaní do zdieÄžanÊho albumu", + "album_added_notification_setting_description": "ObdrÅžaÅĨ upozornenie emailom, keď vÃĄs pridajÃē do zdieÄžanÊho albumu", "album_cover_updated": "Obal albumu aktualizovanÃŊ", "album_delete_confirmation": "Ste si istÃŊ, Åže chcete odstrÃĄniÅĨ album {album}?", "album_delete_confirmation_description": "Ak je tento album zdieÄžanÃŊ, ostatní pouŞívatelia k nemu uÅž nebudÃē maÅĨ prístup.", @@ -376,6 +406,7 @@ "album_options": "Nastavenia albumu", "album_remove_user": "OdstrÃĄniÅĨ pouŞívateÄža?", "album_remove_user_confirmation": "Ste si istÃŊ, Åže chcete odstrÃĄniÅĨ pouŞívateÄža {user}?", + "album_search_not_found": "Neboli nÃĄjdenÊ Åžiadne albumy zodpovedajÃēce vÃĄÅĄmu hÄžadaniu", "album_share_no_users": "VyzerÃĄ to, Åže ste tento album zdieÄžali so vÅĄetkÃŊmi pouŞívateÄžmi alebo nemÃĄte Åžiadneho pouŞívateÄža, s ktorÃŊm by ste ho mohli zdieÄžaÅĨ.", "album_updated": "Album bol aktualizovanÃŊ", "album_updated_setting_description": "ObdrÅžaÅĨ e-mailovÊ upozornenie, keď v zdieÄžanom albume pribudnÃē novÊ poloÅžky", @@ -391,15 +422,19 @@ "album_viewer_page_share_add_users": "PridaÅĨ pouŞívateÄžov", "album_with_link_access": "UmoÅžnite komukoÄžvek s odkazom pozrieÅĨ si fotky a Äžudí v tomto albume.", "albums": "Albumy", - "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albumov}}", + "albums_count": "{count, plural, one {{count, number} album} few {{count, number} albumy} other {{count, number} albumov}}", + "albums_default_sort_order": "PredvolenÊ poradie albumov", + "albums_default_sort_order_description": "PočiatočnÊ poradie triedenia poloÅžiek pri vytvÃĄraní novÃŊch albumov.", + "albums_feature_description": "Zbierky mÊdií, ktorÊ moÅžno zdieÄžaÅĨ s ostatnÃŊmi pouŞívateÄžmi.", + "albums_on_device_count": "Albumy v zariadení ({count})", "all": "VÅĄetko", "all_albums": "VÅĄetky albumy", "all_people": "VÅĄetci Äžudia", "all_videos": "VÅĄetky videa", "allow_dark_mode": "PovoliÅĨ tmavÃŊ reÅžim", "allow_edits": "PovoliÅĨ Ãēpravy", - "allow_public_user_to_download": "PovoÄžte verejnÊmu pouŞívateÄžovi sÅĨahovaÅĨ", - "allow_public_user_to_upload": "UmoÅžniÅĨ verejnÊmu pouŞívateÄžovi nahrÃĄvaÅĨ", + "allow_public_user_to_download": "PovoliÅĨ verejnÊmu pouŞívateÄžovi stiahnutie", + "allow_public_user_to_upload": "UmoÅžniÅĨ verejnÊmu pouŞívateÄžovi nahraÅĨ", "alt_text_qr_code": "ObrÃĄzok QR kÃŗdu", "anti_clockwise": "Proti smeru hodinovÃŊch ručičiek", "api_key": "API KlÃēč", @@ -409,9 +444,10 @@ "app_bar_signout_dialog_content": "Skutočne sa chcete odhlÃĄsiÅĨ?", "app_bar_signout_dialog_ok": "Áno", "app_bar_signout_dialog_title": "OdhlÃĄsiÅĨ sa", - "app_settings": "Nastavenia AplikÃĄcie", + "app_settings": "Nastavenia aplikÃĄcie", "appears_in": "Vyskytuje sa v", - "archive": "ArchivovaÅĨ", + "archive": "Archív", + "archive_action_prompt": "{count} pridanÃŊch do archívu", "archive_or_unarchive_photo": "ArchivÃĄcia alebo odarchivovanie fotografie", "archive_page_no_archived_assets": "ÅŊiadne archivovanÊ mÊdiÃĄ", "archive_page_title": "Archív ({count})", @@ -439,31 +475,45 @@ "asset_list_settings_title": "FotografickÃĄ mrieÅžka", "asset_offline": "MÊdium je offline", "asset_offline_description": "Toto externÃŊ obsah sa uÅž nenachÃĄdza na disku. PoÅžiadajte o pomoc svojho sprÃĄvcu Immich.", + "asset_restored_successfully": "PoloÅžky boli ÃēspeÅĄne obnovenÊ", "asset_skipped": "PreskočenÊ", "asset_skipped_in_trash": "V koÅĄi", "asset_uploaded": "NahranÊ", "asset_uploading": "NahrÃĄva saâ€Ļ", - "asset_viewer_settings_title": "Zobrazovač poloÅžiek", + "asset_viewer_settings_subtitle": "Spravujte nastavenia prehliadača galÊrie", + "asset_viewer_settings_title": "Prehliadač mÊdií", "assets": "PoloÅžky", "assets_added_count": "{count, plural, one {PridanÃĄ # poloÅžka} few {PridanÊ # poloÅžky} other {PridanÃŊch # poloÅžek}}", "assets_added_to_album_count": "Do albumu {count, plural, one {bola pridanÃĄ # poloÅžka} few {boli pridanÊ # poloÅžky} other {bolo pridanÃŊch # poloÅžiek}}", - "assets_added_to_name_count": "{count, plural, one {PridanÃĄ # poloÅžka} few {PridanÊ # poloÅžky} other {PridanÃŊch # poloÅžiek}} do {hasName, select, true {alba {name}} other {novÊho albumu}}", + "assets_cannot_be_added_to_album_count": "{count, plural, one {poloÅžku} other {poloÅžiek}} nie je moÅžnÊ pridaÅĨ do albumu", "assets_count": "{count, plural, one {# poloÅžka} few {# poloÅžky} other {# poloÅžiek}}", + "assets_deleted_permanently": "{count} poloÅžka(iek) natrvalo vymazanÃĄ(ÃŊch)", + "assets_deleted_permanently_from_server": "{count} poloÅžka(iek) natrvalo vymazanÃĄ(ÃŊch) zo servera Immich", + "assets_downloaded_failed": "{count, plural, one {StiahnutÃŊ # sÃēbor - {error} sÃēbor zlyhal} few {StiahnutÊ # sÃēbory - {error} sÃēbory zlyhali} other {StiahnutÃŊch # sÃēborov - {error} sÃēborov zlyhalo}}", + "assets_downloaded_successfully": "{count, plural, one {# sÃēbor bol ÃēspeÅĄne stiahnutÃŊ} few {# sÃēbory boli ÃēspeÅĄne stiahnutÊ} other {# sÃēborov bolo ÃēspeÅĄne stiahnutÃŊch}}", "assets_moved_to_trash_count": "Do koÅĄa {count, plural, one {bola presunutÃĄ # poloÅžka} few {boli presunutÊ # poloÅžky} other {bolo presunutÃŊch # poloÅžiek}}", "assets_permanently_deleted_count": "Trvalo {count, plural, one {vymazanÃĄ # poloÅžka} few {vymazanÊ # poloÅžky} other {vymazanÃŊch # poloÅžiek}}", "assets_removed_count": "{count, plural, one {OdstrÃĄnenÃĄ # poloÅžka} few {OdstrÃĄnenÊ # poloÅžky} other {OdstrÃĄnenÃŊch # poloÅžiek}}", + "assets_removed_permanently_from_device": "{count} poloÅžka(iek) natrvalo vymazanÃĄ(ÃŊch) z vÃĄÅĄho zariadenia", "assets_restore_confirmation": "Naozaj chcete obnoviÅĨ vÅĄetky vyhodenÊ poloÅžky? TÃēto akciu nie je moÅžnÊ vrÃĄtiÅĨ späÅĨ! Upozorňujeme, Åže tÃŊmto spôsobom nie je moÅžnÊ obnoviÅĨ Åžiadne offline poloÅžky.", "assets_restored_count": "{count, plural, one {ObnovenÃĄ # poloÅžka} few {ObnovenÊ # poloÅžky} other {ObnovenÃŊch # poloÅžiek}}", - "assets_restored_successfully": "{count} medií ÃēspeÅĄne obnovenÃŊch", + "assets_restored_successfully": "{count} mÊdií ÃēspeÅĄne obnovenÃŊch", + "assets_trashed": "{count} poloÅžka(iek) vyhodenÃĄ(ÃŊch) do koÅĄa", "assets_trashed_count": "{count, plural, one {OdstrÃĄnenÃĄ # poloÅžka} few {OdstrÃĄnenÊ # poloÅžky} other {OdstrÃĄnenÃŊch # poloÅžiek}}", - "assets_were_part_of_album_count": "{count, plural, one {PoloÅžka bola} other {PoloÅžky boli}} sÃēčasÅĨou albumu", + "assets_trashed_from_server": "{count} poloÅžka(iek) vyhodenÃĄ(ÃŊch) do koÅĄa zo servera Immich", + "assets_were_part_of_album_count": "{count, plural, one {PoloÅžka uÅž bola} other {PoloÅžky uÅž boli}} sÃēčasÅĨou albumu", "authorized_devices": "AutorizovanÊ zariadenia", + "automatic_endpoint_switching_subtitle": "PripojiÅĨ sa lokÃĄlne prostredníctvom určenÊho pripojenia Wi-Fi, ak je k dispozícii, a pouŞívaÅĨ alternatívne pripojenia inde", + "automatic_endpoint_switching_title": "AutomatickÊ prepínanie URL adresy", + "autoplay_slideshow": "AutomatickÊ prehrÃĄvanie prezentÃĄcie", "back": "SpäÅĨ", "back_close_deselect": "SpäÅĨ, zavrieÅĨ alebo zruÅĄiÅĨ vÃŊber", + "background_location_permission": "Povolenie na určenie polohy na pozadí", + "background_location_permission_content": "Aby bolo moÅžnÊ prepínaÅĨ siete pri spustení na pozadí, musí maÅĨ aplikÃĄcia Immich *vÅždy* presnÃŊ prístup k polohe, aby mohla prečítaÅĨ nÃĄzov siete Wi-Fi", "backup_album_selection_page_albums_device": "Albumy v zariadení ({count})", "backup_album_selection_page_albums_tap": "Ťuknutím na poloÅžku ju zahrniete, dvojitÃŊm ÅĨuknutím ju vylÃēčite", "backup_album_selection_page_assets_scatter": "SÃēbory môŞu byÅĨ roztrÃēsenÊ vo viacerÃŊch albumoch. To umoŞňuje zahrnÃēÅĨ alebo vylÃēčiÅĨ albumy počas procesu zÃĄlohovania.", - "backup_album_selection_page_select_albums": "VybranÊ albumy", + "backup_album_selection_page_select_albums": "VybraÅĨ albumy", "backup_album_selection_page_selection_info": "InformÃĄcie o vÃŊbere", "backup_album_selection_page_total_assets": "CelkovÃŊ počet jedinečnÃŊch sÃēborov", "backup_all": "VÅĄetko", @@ -485,7 +535,7 @@ "backup_controller_page_background_charging": "Len počas nabíjania", "backup_controller_page_background_configure_error": "Nepodarilo sa nakonfigurovaÅĨ sluÅžbu na pozadí", "backup_controller_page_background_delay": "Oneskorenie zÃĄlohovania novÃŊch mÊdií: {duration}", - "backup_controller_page_background_description": "PovoÄžte sluÅžbu na pozadí na automatickÊ zÃĄlohovanie vÅĄetkÃŊch novÃŊch aktív bez nutnosti otvorenia aplikÃĄcie", + "backup_controller_page_background_description": "PovoÄžte sluÅžbu na pozadí na automatickÊ zÃĄlohovanie vÅĄetkÃŊch novÃŊch poloÅžiek bez nutnosti otvorenia aplikÃĄcie", "backup_controller_page_background_is_off": "AutomatickÊ zÃĄlohovanie na pozadí je vypnutÊ", "backup_controller_page_background_is_on": "AutomatickÊ zÃĄlohovanie na pozadí je zapnutÊ", "backup_controller_page_background_turn_off": "VypnÃēÅĨ zÃĄlohovanie na pozadí", @@ -503,7 +553,7 @@ "backup_controller_page_info": "InformÃĄcie o zÃĄlohovaní", "backup_controller_page_none_selected": "ÅŊiadne vybranÊ", "backup_controller_page_remainder": "ZostÃĄva", - "backup_controller_page_remainder_sub": "ZostÃĄvajÃēce fotografie a videÃĄ, ktorÊ sa majÃē zÃĄlohovaÅĨ z vÃŊbranÃŊch albumov", + "backup_controller_page_remainder_sub": "ZostÃĄvajÃēce fotografie a videÃĄ, ktorÊ sa majÃē zÃĄlohovaÅĨ z vÃŊberu", "backup_controller_page_server_storage": "ServerovÊ ÃēloÅžisko", "backup_controller_page_start_backup": "SpustiÅĨ zÃĄlohovanie", "backup_controller_page_status_off": "AutomatickÊ zÃĄlohovanie na popredí je vypnutÊ", @@ -521,22 +571,29 @@ "backup_manual_success": "Úspech", "backup_manual_title": "Stav nahrÃĄvania", "backup_options_page_title": "MoÅžnosti zÃĄlohovania", - "backward": "Spätne", + "backup_setting_subtitle": "SpravovaÅĨ nastavenia odosielania na pozadí a v popredí", + "backward": "Dozadu", + "beta_sync": "Stav synchronizÃĄcie verzie Beta", + "beta_sync_subtitle": "SpravovaÅĨ novÃŊ systÊm synchronizÃĄcie", + "biometric_auth_enabled": "BiometrickÊ overovanie je povolenÊ", + "biometric_locked_out": "Ste vymknutí z biometrickÊho overovania", + "biometric_no_options": "Nie sÃē k dispozícii Åžiadne biometrickÊ moÅžnosti", + "biometric_not_available": "BiometrickÊ overenie nie je v tomto zariadení k dispozícii", "birthdate_saved": "DÃĄtum narodenia bol ÃēspeÅĄne uloÅženÃŊ", "birthdate_set_description": "DÃĄtum narodenia sa pouŞíva na vÃŊpočet veku tejto osoby v čase fotografie.", "blurred_background": "RozmazanÊ pozadie", "bugs_and_feature_requests": "Chyby a poÅžiadavky na funkcie", "build": "Zostava", "build_image": "Obraz zostavy", - "bulk_delete_duplicates_confirmation": "Naozaj chcete hromadne odstrÃĄniÅĨ {count, plural, one {# duplikÃĄtnu poloÅžku} few {# duplikÃĄte poloÅžky} other {# duplikÃĄtnych poloÅžiek}}? TÃŊmto sa zachovÃĄ najvÃ¤ÄÅĄia poloÅžka z kaÅždej skupiny a vÅĄetky ostatnÊ duplikÃĄty sa natrvalo odstrÃĄnia. TÃēto akciu nie je moÅžnÊ vrÃĄtiÅĨ späÅĨ!", - "bulk_keep_duplicates_confirmation": "Naozaj chceÅĄ ponechaÅĨ {count, plural, one {# duplicitnÃŊ sÃēbor} other {# duplicitnÊ sÃēbory}}? TÃŊmto sa vysporiadaÅĄ so vÅĄetkÃŊmi duplicitnÃŊmi skupinami bez mazania sÃēborov.", - "bulk_trash_duplicates_confirmation": "Naozaj chcete hromadne vymazaÅĨ {count, plural, one {# duplicitnÃŊ sÃēbor} other {# duplicitnÊ sÃēbory}}? TÃŊmto si ponechÃĄÅĄ z kaÅždej skupiny najvÃ¤ÄÅĄÃ­ sÃēbor a vymaÅžeÅĄ vÅĄetky ostatnÊ duplicitnÊ sÃēbory v skupine.", + "bulk_delete_duplicates_confirmation": "Naozaj chcete hromadne odstrÃĄniÅĨ {count, plural, one {# duplikÃĄtnu poloÅžku} few {# duplikÃĄtne poloÅžky} other {# duplikÃĄtnych poloÅžiek}}? TÃŊmto sa zachovÃĄ najvÃ¤ÄÅĄia poloÅžka z kaÅždej skupiny a vÅĄetky ostatnÊ duplikÃĄty sa natrvalo odstrÃĄnia. TÃēto akciu nie je moÅžnÊ vrÃĄtiÅĨ späÅĨ!", + "bulk_keep_duplicates_confirmation": "Naozaj chcete ponechaÅĨ {count, plural, one {# duplicitnÃē poloÅžku} few {# duplicitnÊ poloÅžky} other {# duplicitnÃŊch poloÅžiek}}? TÃŊmto sa vyrieÅĄia vÅĄetky duplicitnÊ skupiny bez toho, aby sa čokoÄžvek odstrÃĄnilo.", + "bulk_trash_duplicates_confirmation": "Naozaj chcete hromadne vymazaÅĨ {count, plural, one {# duplicitnÃē poloÅžku} few {# duplicitnÊ poloÅžky} other {# duplicitnÃŊch poloÅžiek}}? TÃŊmto sa zachovÃĄ najvÃ¤ÄÅĄia poloÅžka z kaÅždej skupiny a vÅĄetky ostatnÊ duplicitnÊ poloÅžky sa vyhodia.", "buy": "KÃēpiÅĨ Immich", "cache_settings_clear_cache_button": "VymazaÅĨ vyrovnÃĄvaciu pamäÅĨ", "cache_settings_clear_cache_button_title": "VymaÅže vyrovnÃĄvaciu pamäÅĨ aplikÃĄcie. To vÃŊrazne ovplyvní vÃŊkon aplikÃĄcie, kÃŊm sa vyrovnÃĄvacia pamäÅĨ neobnoví.", "cache_settings_duplicated_assets_clear_button": "VYČISTIŤ", - "cache_settings_duplicated_assets_subtitle": "Fotky a videÃĄ ktorÊ sÃē na čiernej listine zvolenÊ aplikÃĄciou", - "cache_settings_duplicated_assets_title": "DuplikÃĄty ({count})", + "cache_settings_duplicated_assets_subtitle": "Fotografie a videÃĄ, ktorÊ aplikÃĄcia ignoruje podÄža zoznamu", + "cache_settings_duplicated_assets_title": "DuplicitnÊ poloÅžky ({count})", "cache_settings_statistics_album": "KniÅžnica nÃĄhÄžadov", "cache_settings_statistics_full": "KompletnÊ fotografie", "cache_settings_statistics_shared": "ZdieÄžanÊ nÃĄhÄžady albumov", @@ -552,9 +609,12 @@ "cancel": "ZruÅĄiÅĨ", "cancel_search": "ZruÅĄiÅĨ vyhÄžadÃĄvanie", "canceled": "ZruÅĄenÊ", + "canceling": "RuÅĄÃ­ sa", "cannot_merge_people": "Nie je moÅžnÊ zlÃēčiÅĨ Äžudí", "cannot_undo_this_action": "TÃēto akciu nemôŞete vrÃĄtiÅĨ späÅĨ!", "cannot_update_the_description": "Popis nie je moÅžnÊ aktualizovaÅĨ", + "cast": "Prenos (cast)", + "cast_description": "Nastavte dostupnÊ ciele prenosu", "change_date": "UpraviÅĨ dÃĄtum", "change_description": "ZmeniÅĨ popis", "change_display_order": "ZmeniÅĨ poradie zobrazenia", @@ -570,9 +630,11 @@ "change_password_form_password_mismatch": "HeslÃĄ sa nezhodujÃē", "change_password_form_reenter_new_password": "Znova zadajte novÊ heslo", "change_pin_code": "ZmeniÅĨ PIN kÃŗd", - "change_your_password": "Zmeňte si heslo", + "change_your_password": "ZmeniÅĨ heslo", "changed_visibility_successfully": "ViditeÄžnosÅĨ bola ÃēspeÅĄne zmenenÃĄ", + "check_corrupt_asset_backup": "SkontrolovaÅĨ, či nie sÃē poÅĄkodenÊ zÃĄlohy poloÅžiek", "check_corrupt_asset_backup_button": "VykonaÅĨ kontrolu", + "check_corrupt_asset_backup_description": "SpustiÅĨ tÃēto kontrolu len cez Wi-Fi a po zÃĄlohovaní vÅĄetkÃŊch poloÅžiek. Tento postup môŞe trvaÅĨ niekoÄžko minÃēt.", "check_logs": "SkontrolovaÅĨ logy", "choose_matching_people_to_merge": "Vyberte rovnakÃŊch Äžudí na zlÃēčenie", "city": "Mesto", @@ -584,6 +646,11 @@ "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "ZadaÅĨ heslo", "client_cert_import": "ImportovaÅĨ", + "client_cert_import_success_msg": "CertifikÃĄt klienta je naimportovanÃŊ", + "client_cert_invalid_msg": "NeplatnÃŊ sÃēbor certifikÃĄtu alebo nesprÃĄvne heslo", + "client_cert_remove_msg": "CertifikÃĄt klienta je odstrÃĄnenÃŊ", + "client_cert_subtitle": "Podporuje iba formÃĄt PKCS12 (.p12, .pfx). Importovanie/odstrÃĄnenie certifikÃĄtu je k dispozícii len pred prihlÃĄsením", + "client_cert_title": "SSL certifikÃĄt klienta", "clockwise": "V smere hodinovÃŊch ručičiek", "close": "ZatvoriÅĨ", "collapse": "ZbaliÅĨ", @@ -607,7 +674,8 @@ "confirm_tag_face": "Chcete označiÅĨ tÃēto tvÃĄr ako {name}?", "confirm_tag_face_unnamed": "Chcete označiÅĨ tÃēto tvÃĄr?", "connected_device": "PripojenÊ zariadenie", - "contain": "ObsiahnÃēÅĨ", + "connected_to": "PripojenÊ k", + "contain": "PrispôsobiÅĨ", "context": "Kontext", "continue": "PokračovaÅĨ", "control_bottom_app_bar_create_new_album": "VytvoriÅĨ novÃŊ album", @@ -616,6 +684,7 @@ "control_bottom_app_bar_edit_location": "UpraviÅĨ polohu", "control_bottom_app_bar_edit_time": "UpraviÅĨ dÃĄtum a čas", "control_bottom_app_bar_share_link": "ZdieÄžaÅĨ odkaz", + "control_bottom_app_bar_share_to": "ZdieÄžaÅĨ cez", "control_bottom_app_bar_trash_from_immich": "PresunÃēÅĨ do koÅĄa", "copied_image_to_clipboard": "ObrÃĄzok skopírovanÃŊ do schrÃĄnky.", "copied_to_clipboard": "SkopírovanÊ do schrÃĄnky!", @@ -627,7 +696,7 @@ "copy_password": "SkopírovaÅĨ heslo", "copy_to_clipboard": "SkopírovaÅĨ do schrÃĄnky", "country": "Krajina", - "cover": "Titulka", + "cover": "VyplniÅĨ", "covers": "DlaÅždice", "create": "VytvoriÅĨ", "create_album": "VytvoriÅĨ album", @@ -635,15 +704,15 @@ "create_library": "VytvoriÅĨ kniÅžnicu", "create_link": "VytvoriÅĨ odkaz", "create_link_to_share": "VytvoriÅĨ odkaz na zdieÄžanie", - "create_link_to_share_description": "UmoÅžniÅĨ kaÅždÊmu kto mÃĄ odkaz zobraziÅĨ vybranÊ fotografie", + "create_link_to_share_description": "UmoÅžniÅĨ kaÅždÊmu, kto mÃĄ odkaz, zobraziÅĨ vybranÊ fotografie", "create_new": "VYTVORIŤ NOVÉ", "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_shared_album_page_share_add_assets": "PridaÅĨ poloÅžky", "create_shared_album_page_share_select_photos": "VybraÅĨ fotografie", - "create_tag": "VytvoriÅĨ značku", - "create_tag_description": "Vytvorenie novÊho ÅĄtítku. Pre VnorenÊ ÅĄtítky, prosím, zadaj celÃē cestu ÅĄtítku, vrÃĄtane lomítok vpred.", + "create_tag": "VytvoriÅĨ ÅĄtítok", + "create_tag_description": "Vytvorte novÃŊ ÅĄtítok. V prípade vnorenÃŊch ÅĄtítkov zadajte celÃē cestu k ÅĄtítku vrÃĄtane lomiek.", "create_user": "VytvoriÅĨ pouŞívateÄža", "created": "VytvorenÊ", "created_at": "VytvorenÊ", @@ -656,7 +725,8 @@ "custom_locale_description": "FormÃĄtovanie dÃĄtumov a čísel podÄža jazyka a regiÃŗnu", "daily_title_text_date": "EEEE, d. MMMM", "daily_title_text_date_year": "EEEE, d. MMMM y", - "dark": "TmavÃŊ", + "dark": "TmavÃĄ", + "dark_theme": "PrepnÃēÅĨ tmavÃē tÊmu", "date_after": "DÃĄtum po", "date_and_time": "DÃĄtum a Čas", "date_before": "DÃĄtum pred", @@ -669,14 +739,15 @@ "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ÃĄ LokalizÃĄcia", - "default_locale_description": "FormÃĄtovanie dÃĄtumu a čísel podÄža lokalizÃĄcie vÃĄÅĄho prehliadača", + "default_locale": "PredvolenÊ miestne nastavenie", + "default_locale_description": "FormÃĄtovanie dÃĄtumov a čísel na zÃĄklade miestneho nastavenia prehliadača", "delete": "VymazaÅĨ", + "delete_action_prompt": "{count} natrvalo vymazanÃŊch", "delete_album": "OdstrÃĄniÅĨ album", "delete_api_key_prompt": "Naozaj chcete odstrÃĄniÅĨ tento API kÄžÃēč?", "delete_dialog_alert": "Tieto poloÅžky budÃē natrvalo odstrÃĄnenÊ z aplikÃĄcie Immich a z vÃĄÅĄho zariadenia", "delete_dialog_alert_local": "Tieto poloÅžky budÃē permanentne vymazanÊ z vaÅĄeho zariadenia, ale budÃē stÃĄle k dispozícií na serveri Immich", - "delete_dialog_alert_local_non_backed_up": "NiektorÊ poloÅžky nie sÃē zÃĄlohovanÊ na Immichi a budÃē permanentne vymazanÊ z vÃĄÅĄho zariadenia", + "delete_dialog_alert_local_non_backed_up": "NiektorÊ poloÅžky nie sÃē zÃĄlohovanÊ na Immich a budÃē permanentne odstrÃĄnenÊ z vÃĄÅĄho zariadenia", "delete_dialog_alert_remote": "Tieto poloÅžky budÃē permanentne vymazanÊ zo serveru Immich", "delete_dialog_ok_force": "Napriek tomu vymazaÅĨ", "delete_dialog_title": "VymazaÅĨ natrvalo", @@ -685,12 +756,13 @@ "delete_key": "OdstrÃĄniÅĨ kÄžÃēč", "delete_library": "VymazaÅĨ kniÅžnicu", "delete_link": "OdstrÃĄniÅĨ odkaz", + "delete_local_action_prompt": "{count} vymazanÊ lokÃĄlne", "delete_local_dialog_ok_backed_up_only": "VymazaÅĨ len zÃĄlohovanÊ", "delete_local_dialog_ok_force": "Napriek tomu vymazaÅĨ", "delete_others": "VymazaÅĨ ostatnÊ", "delete_shared_link": "OdstrÃĄniÅĨ zdieÄžanÃŊ odkaz", "delete_shared_link_dialog_title": "OdstrÃĄniÅĨ zdieÄžanÃŊ odkaz", - "delete_tag": "OdstrÃĄniÅĨ označenie", + "delete_tag": "OdstrÃĄniÅĨ ÅĄtítok", "delete_tag_confirmation_prompt": "Naozaj chcete odstrÃĄniÅĨ ÅĄtítok menom {tagName}?", "delete_user": "VymazaÅĨ pouŞívateÄža", "deleted_shared_link": "VymazanÃŊ zdieÄžanÃŊ odkaz", @@ -698,6 +770,7 @@ "description": "Popis", "description_input_hint_text": "PridaÅĨ popis...", "description_input_submit_error": "Chyba pri aktualizovaní popisu, zobrazte log pre viac detailov", + "deselect_all": "ZruÅĄiÅĨ vÃŊber vÅĄetkÃŊch", "details": "Podrobnosti", "direction": "Smer", "disabled": "VypnutÊ", @@ -715,22 +788,27 @@ "documentation": "DokumentÃĄcia", "done": "Hotovo", "download": "StiahnuÅĨ", + "download_action_prompt": "SÅĨahuje sa {count} poloÅžiek", "download_canceled": "Stiahnutie zruÅĄenÊ", "download_complete": "Stiahnutie dokončenÊ", + "download_enqueue": "Stiahnutie v poradí", "download_error": "Chyba sÅĨahovania", "download_failed": "Stiahnutie sa nepodarilo", "download_finished": "Stiahnutie dokončenÊ", "download_include_embedded_motion_videos": "VloÅženÊ videÃĄ", "download_include_embedded_motion_videos_description": "ZahrnÃēÅĨ videÃĄ vloÅženÊ do pohyblivÃŊch fotiek ako samostatnÊ sÃēbory", + "download_notfound": "Stiahnutie nebolo nÃĄjdenÊ", "download_paused": "Stiahnutie pozastavenÊ", "download_settings": "StiahnuÅĨ", "download_settings_description": "SpravovaÅĨ nastavenia sÃēvisiace so sÅĨahovaním poloÅžiek", "download_started": "SÅĨahovanie spustenÊ", + "download_sucess": "Stiahnutie ÃēspeÅĄnÊ", "download_sucess_android": "MÊdiÃĄ boli stiahnutÊ do DCIM/Immich", + "download_waiting_to_retry": "ČakÃĄ sa na opakovanie pokusu", "downloading": "SÅĨahuje sa", "downloading_asset_filename": "SÅĨahuje sa poloÅžka {filename}", "downloading_media": "SÅĨahovanie mÊdií", - "drop_files_to_upload": "Hoď sÃēbory kdekoÄžvek, nahrajÃē sa", + "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Ê", "duration": "Trvanie", @@ -748,10 +826,11 @@ "edit_key": "UpraviÅĨ kÄžÃēč", "edit_link": "UpraviÅĨ odkaz", "edit_location": "UpraviÅĨ polohu", + "edit_location_action_prompt": "{count} poloha upravenÃĄ", "edit_location_dialog_title": "Poloha", "edit_name": "UpraviÅĨ meno", "edit_people": "UpraviÅĨ osoby", - "edit_tag": "UpraiÅĨ značku", + "edit_tag": "UpraviÅĨ ÅĄtítok", "edit_title": "UpraviÅĨ nÃĄzov", "edit_user": "UpraviÅĨ pouŞívateÄža", "edited": "UpravenÊ", @@ -766,23 +845,28 @@ "empty_trash": "VyprÃĄzdniÅĨ kÃ´ÅĄ", "empty_trash_confirmation": "Naozaj chcete vyprÃĄzdniÅĨ kÃ´ÅĄ? NenÃĄvratne sa vymaÅžÃē vÅĄetky poloÅžky z Immich.\nTÃĄto akcia sa nedÃĄ vrÃĄtiÅĨ!", "enable": "AktivovaÅĨ", + "enable_backup": "PovoliÅĨ zÃĄlohovanie", + "enable_biometric_auth_description": "Zadajte svoj PIN kÃŗd, aby ste povolili biometrickÊ overenie", "enabled": "AktivovanÃŊ", "end_date": "KoncovÃŊ dÃĄtum", + "enqueued": "V poradí", "enter_wifi_name": "Zadajte nÃĄzov Wi-Fi", "enter_your_pin_code": "Zadajte svoj PIN kÃŗd", "enter_your_pin_code_subtitle": "Zadaním kÃŗdu PIN získate prístup k zamknutÊmu priečinku", "error": "Chyba", + "error_change_sort_album": "Nepodarilo sa zmeniÅĨ poradie albumu", "error_delete_face": "Chyba pri odstraňovaní tvÃĄre z poloÅžky", "error_loading_image": "Nepodarilo sa načítaÅĨ obrÃĄzok", "error_saving_image": "Chyba: {error}", + "error_tag_face_bounding_box": "Chyba pri označovaní tvÃĄre - nemoÅžno získaÅĨ sÃēradnice ohraničujÃēceho poÄža", "error_title": "Chyba - niečo sa pokazilo", "errors": { "cannot_navigate_next_asset": "NedokÃĄÅžem prejsÅĨ na ďaÄžÅĄiu poloÅžku", "cannot_navigate_previous_asset": "NedokÃĄÅžem prejsÅĨ na predoÅĄlÃē poloÅžku", "cant_apply_changes": "NedokÃĄÅžem aplikovaÅĨ zmeny", - "cant_change_activity": "NodokÃĄÅžem {enabled, select, true {zakÃĄzaÅĨ} other {povoliÅĨ}} aktivitu", + "cant_change_activity": "Nie je moÅžnÊ {enabled, select, true {zakÃĄzaÅĨ} other {povoliÅĨ}} aktivitu", "cant_change_asset_favorite": "NedokÃĄÅžem zmeniÅĨ obÄžÃēbenosÅĨ pre poloÅžku", - "cant_change_metadata_assets_count": "NedokÃĄÅžem zmeniÅĨ metadÃĄta pre {count, plural, one {# tÃēto poloÅžku} other {# tieto poloÅžky}}", + "cant_change_metadata_assets_count": "Nie je moÅžnÊ zmeniÅĨ metadÃĄta pre {count, plural, one {# poloÅžku} few {# poloÅžky} other {# poloÅžiek}}", "cant_get_faces": "NedokÃĄÅžem získaÅĨ tvÃĄre", "cant_get_number_of_comments": "NedokÃĄÅžem získaÅĨ počet komentÃĄrov", "cant_search_people": "NedokÃĄÅžem hÄžadaÅĨ osoby", @@ -802,10 +886,12 @@ "failed_to_keep_this_delete_others": "Nepodarilo sa ponechaÅĨ tÃēto poloÅžku a vymazaÅĨ tie ostatnÊ poloÅžky", "failed_to_load_asset": "Nepodarilo sa načítaÅĨ poloÅžku", "failed_to_load_assets": "Nepodarilo sa načítaÅĨ poloÅžky", + "failed_to_load_notifications": "Nepodarilo sa načítaÅĨ oznÃĄmenia", "failed_to_load_people": "Nepodarilo sa načítaÅĨ Äžudí", "failed_to_remove_product_key": "Nepodarilo sa odstrÃĄniÅĨ produktovÃŊ kÄžÃēč", "failed_to_stack_assets": "Nepodarilo sa zoskupiÅĨ poloÅžky", "failed_to_unstack_assets": "Nepodarilo sa rozdeliÅĨ poloÅžky", + "failed_to_update_notification_status": "Nepodarilo sa aktualizovaÅĨ stav oznÃĄmenia", "import_path_already_exists": "TÃĄto cesta importu uÅž existuje.", "incorrect_email_or_password": "NesprÃĄvny e-mail alebo heslo", "paths_validation_failed": "{paths, plural, one {# cesta zlyhala} few {# cesty zlyhali} other {# ciest zlyhalo}} pri validÃĄcii", @@ -826,14 +912,14 @@ "unable_to_change_favorite": "Nie je moÅžnÊ zmeniÅĨ obÄžÃēbenÊ pre poloÅžku", "unable_to_change_location": "Nie je moÅžnÊ zmeniÅĨ polohu", "unable_to_change_password": "Nie je moÅžnÊ zmeniÅĨ heslo", - "unable_to_change_visibility": "Nie je moÅžnÊ zmeniÅĨ viditeÄžnosÅĨ pre {count, plural, one {# osobu} other {# Äžudí}}", + "unable_to_change_visibility": "Nie je moÅžnÊ zmeniÅĨ viditeÄžnosÅĨ pre {count, plural, one {# osobu} few {# osoby} other {# osôb}}", "unable_to_complete_oauth_login": "NemoÅžno dokončiÅĨ prihlÃĄsenie cez OAuth", "unable_to_connect": "Nie je moÅžnÊ sa pripojiÅĨ", "unable_to_copy_to_clipboard": "Nie je moÅžnÊ kopírovaÅĨ do schrÃĄnky, overte si, Åže strÃĄnku navÅĄtevujete cez https", - "unable_to_create_admin_account": "Nie je moÅžnÊ vytvoriÅĨ admin Ãēčet", + "unable_to_create_admin_account": "Nie je moÅžnÊ vytvoriÅĨ Ãēčet sprÃĄvcu", "unable_to_create_api_key": "Nie je moÅžnÊ vytvoriÅĨ novÃŊ API KlÃēč", "unable_to_create_library": "Nie je moÅžnÊ vytvoriÅĨ knihovňu", - "unable_to_create_user": "Nie je moÅžnÊ vytvoriÅĨ uÅživateÄža", + "unable_to_create_user": "Nie je moÅžnÊ vytvoriÅĨ pouŞívateÄža", "unable_to_delete_album": "Nie je moÅžnÊ vymazaÅĨ album", "unable_to_delete_asset": "Nie je moÅžnÊ vymazaÅĨ poloÅžku", "unable_to_delete_assets": "Chyba pri odstraňovaní poloÅžiek", @@ -842,7 +928,7 @@ "unable_to_delete_shared_link": "Nie je moÅžnÊ vymazaÅĨ zdieÄžanÃŊ odkaz", "unable_to_delete_user": "Nie je moÅžnÊ vymazaÅĨ pouŞívateÄža", "unable_to_download_files": "Nie je moÅžnÊ stiahnuÅĨ sÃēbory", - "unable_to_edit_exclusion_pattern": "Nie je moÅžnÊ upravit vzorec vylÃēčenia", + "unable_to_edit_exclusion_pattern": "Nie je moÅžnÊ upraviÅĨ vzorec vylÃēčenia", "unable_to_edit_import_path": "Nie je moÅžnÊ upraviÅĨ cestu importu", "unable_to_empty_trash": "Nie je moÅžnÊ vyprÃĄzdniÅĨ kÃ´ÅĄ", "unable_to_enter_fullscreen": "Nie je moÅžnÊ prejsÅĨ do reÅžimu celej obrazovky", @@ -865,7 +951,7 @@ "unable_to_remove_library": "Nie je moÅžnÊ odstrÃĄniÅĨ kniÅžnicu", "unable_to_remove_partner": "Nie je moÅžnÊ odstrÃĄniÅĨ partnera", "unable_to_remove_reaction": "Nie je moÅžnÊ odstrÃĄniÅĨ reakciu", - "unable_to_reset_password": "Nie je moÅžnÊ resetovaÅĨ heslo", + "unable_to_reset_password": "Nie je moÅžnÊ obnoviÅĨ heslo", "unable_to_reset_pin_code": "Nie je moÅžnÊ obnoviÅĨ PIN kÃŗd", "unable_to_resolve_duplicate": "Nie je moÅžnÊ vyrieÅĄiÅĨ duplikÃĄt", "unable_to_restore_assets": "Nie je moÅžnÊ obnoviÅĨ poloÅžky", @@ -900,15 +986,16 @@ "exif_bottom_sheet_location": "POLOHA", "exif_bottom_sheet_people": "ÄŊUDIA", "exif_bottom_sheet_person_add_person": "PridaÅĨ meno", + "exif_bottom_sheet_person_age_months": "Vek {months} mesiacov", "exif_bottom_sheet_person_age_year_months": "Vek 1 rok, {months} mesiacov", "exif_bottom_sheet_person_age_years": "Vek {years}", - "exit_slideshow": "OpustiÅĨ Slideshow", + "exit_slideshow": "OpustiÅĨ prezentÃĄciu", "expand_all": "RozbaliÅĨ vÅĄetko", "experimental_settings_new_asset_list_subtitle": "PrebiehajÃēca prÃĄca", "experimental_settings_new_asset_list_title": "Povolenie experimentÃĄlnej mrieÅžky fotografií", "experimental_settings_subtitle": "PouŞívajte na vlastnÊ riziko!", "experimental_settings_title": "ExperimentÃĄlne", - "expire_after": "Expiruje po", + "expire_after": "PlatnosÅĨ vyprÅĄÃ­", "expired": "VyprÅĄalo", "expires_date": "Expiruje {date}", "explore": "PreskÃēmaÅĨ", @@ -922,17 +1009,20 @@ "external_network_sheet_info": "Ak nie ste v preferovanej sieti Wi-Fi, aplikÃĄcia sa pripojí k serveru prostredníctvom prvej z niÅžÅĄie uvedenÃŊch adries URL, na ktorÃē sa dostane, počnÃēc zhora nadol", "face_unassigned": "NepriradenÃĄ", "failed": "NeÃēspeÅĄnÊ", + "failed_to_authenticate": "Nepodarilo sa overiÅĨ", "failed_to_load_assets": "Nepodarilo sa načítaÅĨ poloÅžky", + "failed_to_load_folder": "Nepodarilo sa načítaÅĨ priečinok", "favorite": "ObÄžÃēbenÊ", + "favorite_action_prompt": "{count} pridanÊ do obÄžÃēbenÃŊch", "favorite_or_unfavorite_photo": "OznačiÅĨ fotku ako obÄžÃēbenÃē alebo neobÄžÃēbenÃē", "favorites": "ObÄžÃēbenÊ", "favorites_page_no_favorites": "ÅŊiadne obÄžÃēbenÊ mÊdiÃĄ", "feature_photo_updated": "HlavnÃŊ obrÃĄzok bol aktualizovanÃŊ", "features": "Funkcie", "features_setting_description": "SpravovaÅĨ funkcie aplikÃĄcie", - "file_name": "Meno sÃēboru", + "file_name": "NÃĄzov sÃēboru", "file_name_or_extension": "NÃĄzov alebo prípona sÃēboru", - "filename": "Meno sÃēboru", + "filename": "NÃĄzov sÃēboru", "filetype": "Typ sÃēboru", "filter": "Filter", "filter_people": "FiltrovaÅĨ Äžudí", @@ -940,11 +1030,15 @@ "find_them_fast": "NÃĄjdite ich rÃŊchlejÅĄie podÄža mena", "fix_incorrect_match": "OpraviÅĨ nesprÃĄvnu zhodu", "folder": "Priečinok", + "folder_not_found": "Priečinok nebol nÃĄjdenÃŊ", "folders": "Priečinky", - "folders_feature_description": "Prehliadanie zobrazenia priečinka s fotografiami a videami na sÃēborovom systÊme", + "folders_feature_description": "Prezeranie zobrazenia priečinkov fotografií a videí v systÊme sÃēborov", "forward": "Dopredu", + "gcast_enabled": "Google Cast", + "gcast_enabled_description": "TÃĄto funkcia načítava externÊ zdroje zo spoločnosti Google, aby mohla fungovaÅĨ.", "general": "VÅĄeobecnÊ", "get_help": "ZískaÅĨ pomoc", + "get_wifiname_error": "Nepodarilo sa získaÅĨ nÃĄzov Wi-Fi siete. Uistite sa, Åže ste udelili potrebnÊ oprÃĄvnenia a ste pripojení k sieti Wi-Fi", "getting_started": "Začíname", "go_back": "VrÃĄtiÅĨ sa späÅĨ", "go_to_folder": "PrejsÅĨ do priečinka", @@ -959,6 +1053,15 @@ "haptic_feedback_switch": "PovoliÅĨ hmatovÃē odozvu", "haptic_feedback_title": "HmatovÃĄ odozva", "has_quota": "MÃĄ kvÃŗtu", + "hash_asset": "HashovaÅĨ poloÅžku", + "hashed_assets": "HashovanÊ poloÅžky", + "hashing": "Hashovanie", + "header_settings_add_header_tip": "PridaÅĨ hlavičku", + "header_settings_field_validator_msg": "Hodnota nemôŞe byÅĨ prÃĄzdna", + "header_settings_header_name_input": "NÃĄzov hlavičky", + "header_settings_header_value_input": "Hodnota hlavičky", + "headers_settings_tile_subtitle": "Určite hlavičky proxy servera, ktorÊ mÃĄ aplikÃĄcia posielaÅĨ s kaÅždou poÅžiadavkou na sieÅĨ", + "headers_settings_tile_title": "VlastnÊ hlavičky proxy servera", "hi_user": "Ahoj {name} ({email})", "hide_all_people": "SkryÅĨ vÅĄetky osoby", "hide_gallery": "SkryÅĨ galÊriu", @@ -974,25 +1077,32 @@ "home_page_archive_err_partner": "NemoÅžno archivovaÅĨ partnerskÊ poloÅžky, preskakuje sa", "home_page_building_timeline": "VytvÃĄranie časovej osi", "home_page_delete_err_partner": "Nie je moÅžnÊ vymazaÅĨ poloÅžky partnera, preskakuje sa", + "home_page_delete_remote_err_local": "Miestne poloÅžky vo vÃŊbere vzdialenÊho odstrÃĄnenia, preskakuje sa", "home_page_favorite_err_local": "ZatiaÄž nie je moÅžnÊ zaradiÅĨ lokÃĄlne mÊdia medzi obÄžÃēbenÊ, preskakuje sa", "home_page_favorite_err_partner": "Na teraz nemôŞete pridaÅĨ partnerove mÊdiÃĄ medzi obÄžÃēbenÊ", "home_page_first_time_notice": "Ak aplikÃĄciu pouŞívate prvÃŊkrÃĄt, uistite sa, Åže ste si vybrali zÃĄloÅžnÃŊ album, aby sa na časovej osi mohli zobrazovaÅĨ fotografie a videÃĄ", + "home_page_locked_error_local": "Nie je moÅžnÊ presunÃēÅĨ miestne poloÅžky do zamknutÊho priečinka, preskakuje sa", + "home_page_locked_error_partner": "Nie je moÅžnÊ presunÃēÅĨ partnerskÊ poloÅžky do zamknutÊho priečinka, preskakuje sa", "home_page_share_err_local": "NemoÅžno zdieÄžaÅĨ lokÃĄlne mÊdiÃĄ pomocou odkazu", "home_page_upload_err_limit": "Naraz môŞete nahraÅĨ len 30 mÊdií, preskakuje sa", "host": "HostiteÄž", "hour": "Hodina", "id": "ID", + "idle": "NečinnÊ", + "ignore_icloud_photos": "IgnorovaÅĨ fotky v sluÅžbe iCloud", + "ignore_icloud_photos_description": "Fotografie uloÅženÊ v sluÅžbe iCloud sa nebudÃē odosielaÅĨ na server Immich", "image": "ObrÃĄzok", "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} nasnímanÊ {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} nasnímanÊ s {person1} dňa {date}", "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} nasnímanÊ s {person1} a {person2} dňa {date}", "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} nasnímanÊ s {person1}, {person2} a {person3} dňa {date}", - "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} nasnímanÊ s {person1}, {person2} a {additionalCount, number} inÃŊmi dňa {date}", - "image_alt_text_date_place": "{isVideo, select, true {Video} other {ObrÃĄzok}} nasnímanÊ v {city}, {country} dňa {date}", - "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {ObrÃĄzok}} zo dňa {date} v {city}, {country} s {person1}", - "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {ObrÃĄzok}} v {city}, {country} s {person1} a {person2} zo dňa {date}", - "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {ObrÃĄzok}} zo dňa {date} v {city}, {country} s {person1}, {person2} a {person3}", - "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} nasnímanÃŊ v {city}, {country} s {person1}, {person2} a {additionalCount, number} inÃŊmi dňa {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video nasnímanÊ} other {ObrÃĄzok odfotenÃŊ}} s osobami {person1}, {person2} a {additionalCount, number} inÃŊmi dňa {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video nasnímanÊ} other {ObrÃĄzok odfotenÃŊ}} v {city}, {country} dňa {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video nasnímanÊ} other {ObrÃĄzok odfotenÃŊ}} dňa {date} v {city}, {country} s {person1}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video nasnímanÊ} other {ObrÃĄzok odfotenÃŊ}} v {city}, {country} s {person1} a {person2} dňa {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video nasnímanÊ} other {ObrÃĄzok odfotenÃŊ}} dňa {date} v {city}, {country} s {person1}, {person2} a {person3}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video nasnímamÊ} other {ObrÃĄzok odfotenÃŊ}} v {city}, {country} s {person1}, {person2} a {additionalCount, number} inÃŊmi dňa {date}", + "image_saved_successfully": "ObrÃĄzok bol uloÅženÃŊ", "image_viewer_page_state_provider_download_started": "SÅĨahovanie sa začalo", "image_viewer_page_state_provider_download_success": "SÅĨahovanie bolo ÃēspeÅĄnÊ", "image_viewer_page_state_provider_share_error": "Chyba zdieÄžania", @@ -1015,17 +1125,27 @@ "night_at_twoam": "KaÅždÃē noc o 2:00" }, "invalid_date": "NeplatnÃŊ dÃĄtum", + "invalid_date_format": "NeplatnÃŊ formÃĄt dÃĄtumu", "invite_people": "PozvaÅĨ Äžudí", "invite_to_album": "PozvaÅĨ do albumu", + "ios_debug_info_fetch_ran_at": "Načítanie prebehlo {dateTime}", + "ios_debug_info_last_sync_at": "PoslednÃĄ synchronizÃĄcia {dateTime}", + "ios_debug_info_no_processes_queued": "ÅŊiadne procesy nie sÃē v poradí na pozadí", + "ios_debug_info_no_sync_yet": "ZatiaÄž nebola spustenÃĄ Åžiadna Ãēloha synchronizÃĄcie na pozadí", + "ios_debug_info_processes_queued": "{count, plural, one {{count} proces na pozadí v poradí} few {{count} procesy na pozadí v poradí} other {{count} procesov na pozadí v poradí}}", + "ios_debug_info_processing_ran_at": "Spracovanie prebehlo {dateTime}", "items_count": "{count, plural, one {# poloÅžka} few {# poloÅžky} other {# poloÅžiek}}", "jobs": "Úlohy", "keep": "PonechaÅĨ", "keep_all": "PonechaÅĨ vÅĄetko", "keep_this_delete_others": "PonechaÅĨ toto, odstrÃĄniÅĨ ostatnÊ", - "kept_this_deleted_others": "PonechÃĄ tÃēto poloÅžku a odstrÃĄni {count, plural, one {# poloÅžku} other {# poloÅžiek}}", + "kept_this_deleted_others": "Ponechal tÃēto poloÅžku a odstrÃĄnil {count, plural, one {# poloÅžku} few {# poloÅžky} other {# poloÅžiek}}", "keyboard_shortcuts": "KlÃĄvesovÊ skratky", "language": "Jazyk", - "language_setting_description": "Vyberte preferovanÃŊ jazyk", + "language_no_results_subtitle": "SkÃēste upraviÅĨ hÄžadanÃŊ vÃŊraz", + "language_no_results_title": "Neboli nÃĄjdenÊ Åžiadne jazyky", + "language_search_hint": "VyhÄžadaÅĨ jazyky...", + "language_setting_description": "Vyberte poÅžadovanÃŊ jazyk", "last_seen": "Naposledy videnÊ", "latest_version": "NajnovÅĄia verzia", "latitude": "ZemepisnÃĄ ÅĄÃ­rka", @@ -1041,7 +1161,8 @@ "library_page_sort_created": "NajnovÅĄie vytvorenÊ", "library_page_sort_last_modified": "Naposledy upravenÊ", "library_page_sort_title": "PodÄža nÃĄzvu albumu", - "light": "SvetlÃŊ", + "licenses": "Licencie", + "light": "SvetlÃĄ", "like_deleted": "Like odstrÃĄnenÃŊ", "link_motion_video": "PripojiÅĨ pohyblivÊ video", "link_options": "MoÅžnosti odkazu", @@ -1050,7 +1171,12 @@ "list": "Zoznam", "loading": "Načítavanie", "loading_search_results_failed": "Načítanie vÃŊsledkov hÄžadania sa nepodarilo", + "local": "LokÃĄlne", + "local_asset_cast_failed": "Nie je moÅžnÊ preniesÅĨ mÊdium, ktorÊ nie je nahranÊ na serveri", + "local_assets": "LokÃĄlne poloÅžky", "local_network": "Miestna sieÅĨ", + "local_network_sheet_info": "Pri pouÅžití zadanej siete Wi-Fi sa aplikÃĄcia pripojí k serveru prostredníctvom tejto URL adresy", + "location_permission": "Povolenie na určenie polohy", "location_permission_content": "Na pouŞívanie funkcie automatickÊho prepínania potrebuje aplikÃĄcia Immich presnÊ povolenie na určenie polohy, aby mohla prečítaÅĨ nÃĄzov aktuÃĄlnej Wi-Fi siete", "location_picker_choose_on_map": "ZvoÄžte na mape", "location_picker_latitude_error": "Zadajte platnÃē zemepisnÃē ÅĄÃ­rku", @@ -1061,6 +1187,7 @@ "locked_folder": "ZamknutÃŊ priečinok", "log_out": "OdhlÃĄsiÅĨ sa", "log_out_all_devices": "OdhlÃĄsiÅĨ vÅĄetky zariadenia", + "logged_in_as": "PrihlÃĄsenÃŊ ako {user}", "logged_out_all_devices": "VÅĄetky zariadenia odhlÃĄsenÊ", "logged_out_device": "Zariadenie odhlÃĄsenÊ", "login": "PrihlÃĄsenie", @@ -1069,7 +1196,7 @@ "login_form_back_button_text": "SpäÅĨ", "login_form_email_hint": "tvojmail@email.com", "login_form_endpoint_hint": "http://ip-tvojho-servera:port", - "login_form_endpoint_url": "URL adresa servera", + "login_form_endpoint_url": "URL adresa koncovÊho bodu servera", "login_form_err_http": "Prosím, uveďte http:// alebo https://", "login_form_err_invalid_email": "NeplatnÃŊ e-mail", "login_form_err_invalid_url": "NeplatnÃĄ URL adresa", @@ -1089,7 +1216,7 @@ "logout_all_device_confirmation": "Ste si istÃŊ, Åže sa chcete odhlÃĄsiÅĨ zo vÅĄetkÃŊch zariadení?", "logout_this_device_confirmation": "Ste si istÃŊ, Åže sa chcete odhlÃĄsiÅĨ z tohoto zariadenia?", "longitude": "ZemepisnÃĄ dÄēÅžka", - "look": "Zobrazenie", + "look": "VzhÄžad", "loop_videos": "OpakovaÅĨ videÃĄ", "loop_videos_description": "Povolí prehrÃĄvanie videí v slučke v detailnom zobrazení.", "main_branch_warning": "PouŞívate vÃŊvojÃĄrsku verziu; dôrazne odporÃēčame pouŞívaÅĨ vydanÊ verzie!", @@ -1127,6 +1254,9 @@ "map_settings_only_show_favorites": "ZobraziÅĨ iba obÄžÃēbenÊ", "map_settings_theme_settings": "TÊma mapy", "map_zoom_to_see_photos": "OddiaÄžte priblíŞenie aby ste videli fotky", + "mark_all_as_read": "OznačiÅĨ vÅĄetko ako prečítanÊ", + "mark_as_read": "OznačiÅĨ ako prečítanÊ", + "marked_all_as_read": "OznačenÊ vÅĄetko ako prečítanÊ", "matches": "Zhody", "media_type": "Typ mÊdia", "memories": "Spomienky", @@ -1143,7 +1273,7 @@ "merge_people_limit": "ZlÃēčiÅĨ môŞete naraz najviac 5 tvÃĄrí", "merge_people_prompt": "Chcete zlÃēčiÅĨ tÃŊchto Äžudí? TÃĄto akcia sa nedÃĄ vrÃĄtiÅĨ.", "merge_people_successfully": "ZlÃēčenie Äžudí sa podarilo", - "merged_people_count": "ZlÃēčení {count, plural, one {# človek} other {# Äžudia}}", + "merged_people_count": "{count, plural, one {ZlÃēčenÃĄ # osoba} few {ZlÃēčenÊ # osoby} other {ZlÃēčenÃŊch # osôb}}", "minimize": "MinimalizovaÅĨ", "minute": "MinÃēta", "missing": "ChÃŊbajÃēce", @@ -1153,15 +1283,20 @@ "more": "Viac", "move": "PresunÃēÅĨ", "move_off_locked_folder": "PresunÃēÅĨ zo zamknutÊho priečinka", + "move_to_lock_folder_action_prompt": "{count} pridanÃŊch do zamknutÊho priečinka", "move_to_locked_folder": "PresunÃēÅĨ do zamknutÊho priečinka", - "move_to_locked_folder_confirmation": "Tieto fotografie a videÃĄ budÃē odstrÃĄnenÊ zo vÅĄetkÃŊch albumov a bude ich moÅžnÊ zobraziÅĨ len v zamknutom priečinku", + "move_to_locked_folder_confirmation": "Tieto fotografie a videÃĄ budÃē odobranÊ zo vÅĄetkÃŊch albumov a bude ich moÅžnÊ zobraziÅĨ len v zamknutom priečinku", + "moved_to_archive": "{count, plural, one {PresunutÃĄ # poloÅžka} few {PresunutÊ # poloÅžky} other {PresunutÃŊch # poloÅžiek}} do archívu", + "moved_to_library": "{count, plural, one {PresunutÃĄ # poloÅžka} few {PresunutÊ # poloÅžky} other {PresunutÃŊch # poloÅžiek}} do kniÅžnice", "moved_to_trash": "PresunutÊ do koÅĄa", "multiselect_grid_edit_date_time_err_read_only": "NemoÅžno upraviÅĨ dÃĄtum poloÅžky len na čítanie, preskakujem", - "multiselect_grid_edit_gps_err_read_only": "Nie je moÅžnÊ upraviÅĨ polohu poloÅžky (poloÅžiek) len na čítanie, preskakuje sa", + "multiselect_grid_edit_gps_err_read_only": "Nie je moÅžnÊ upraviÅĨ polohu poloÅžky (poloÅžiek), ktorÃĄ je len na čítanie, preskakuje sa", "mute_memories": "Vyblednutie spomienok", "my_albums": "Moje albumy", "name": "Meno", "name_or_nickname": "Meno alebo prezÃŊvka", + "networking_settings": "SieÅĨ", + "networking_subtitle": "SpravovaÅĨ nastavenia koncovÊho bodu servera", "never": "nikdy", "new_album": "NovÃŊ album", "new_api_key": "NovÃŊ API kÄžÃēč", @@ -1178,9 +1313,10 @@ "no_albums_message": "Vytvorí album na organizovanie fotiek a videí", "no_albums_with_name_yet": "VyzerÃĄ, Åže zatiaÄž nemÃĄte album s tÃŊmto nÃĄzvom.", "no_albums_yet": "VyzerÃĄ, Åže zatiaÄž nemÃĄte Åžiadne albumy.", - "no_archived_assets_message": "ArchivovaÅĨ fotografie a videÃĄ, aby sa skryli zo zobrazenia Fotografie", + "no_archived_assets_message": "Archivujte fotografie a videÃĄ a skryte ich z vÃĄÅĄho zobrazenia fotografií", "no_assets_message": "KLIKNITE A NAHRAJTE SVOJU PRVÚ FOTKU", "no_assets_to_show": "ÅŊiadne poloÅžky", + "no_cast_devices_found": "NenaÅĄli sa Åžiadne zariadenia na prenos", "no_duplicates_found": "NenaÅĄli sa Åžiadne duplicity.", "no_exif_info_available": "Nie sÃē dostupnÊ exif Ãēdaje", "no_explore_results_message": "Nahrajte viac fotiek na objavovanie vaÅĄej zbierky.", @@ -1188,16 +1324,21 @@ "no_libraries_message": "Vytvorí externÃē kniÅžnicu na prezeranie fotiek a videí", "no_locked_photos_message": "Fotografie a videÃĄ v zamknutom priečinku sÃē skrytÊ a nezobrazujÃē sa pri prehÄžadÃĄvaní alebo vyhÄžadÃĄvaní v kniÅžnici.", "no_name": "Bez mena", + "no_notifications": "ÅŊiadne oznÃĄmenia", + "no_people_found": "NenaÅĄli sa Åžiadni vyhovujÃēci Äžudia", "no_places": "Bez miesta", "no_results": "ÅŊiadne vÃŊsledky", "no_results_description": "SkÃēste synonymum alebo vÅĄeobecnejÅĄÃ­ vÃŊraz", "no_shared_albums_message": "Vytvorí album na zdieÄžanie fotiek a videí s Äžuďmi vo vaÅĄej sieti", + "no_uploads_in_progress": "ÅŊiadne prebiehajÃēce nahrÃĄvanie", "not_in_any_album": "Nie je v Åžiadnom albume", + "not_selected": "NevybranÊ", "note_apply_storage_label_to_previously_uploaded assets": "PoznÃĄmka: Ak chcete pouÅžiÅĨ Å títok ÃēloÅžiska na predtÃŊm nahranÊ mÊdiÃĄ, spustite príkaz", "notes": "PoznÃĄmky", + "nothing_here_yet": "ZatiaÄž tu nič nie je", "notification_permission_dialog_content": "Ak chcete povoliÅĨ upozornenia, prejdite do Nastavenia a vyberte moÅžnosÅĨ PovoliÅĨ.", - "notification_permission_list_tile_content": "UdeÄžte oprÃĄvnenie k aktivÃĄcii oznÃĄmení.", - "notification_permission_list_tile_enable_button": "PovoliÅĨ upozornenia", + "notification_permission_list_tile_content": "UdeÄžte povolenie na zapnutie oznÃĄmení.", + "notification_permission_list_tile_enable_button": "PovoliÅĨ oznÃĄmenia", "notification_permission_list_tile_title": "Povolenie oznÃĄmení", "notification_toggle_setting_description": "PovoliÅĨ e-mailovÊ upozornenia", "notifications": "OznÃĄmenia", @@ -1209,7 +1350,9 @@ "oldest_first": "NajstarÅĄie prvÊ", "on_this_device": "Na tomto zariadení", "onboarding": "Na palube", + "onboarding_locale_description": "Vyberte poÅžadovanÃŊ jazyk. Neskôr ho môŞete zmeniÅĨ v nastaveniach.", "onboarding_privacy_description": "NasledujÃēce (voliteÄžnÊ) funkcie zÃĄvisia na externÃŊch sluÅžbÃĄch a kedykoÄžvek ich môŞete vypnÃēÅĨ nastaveniach.", + "onboarding_server_welcome_description": "Poďme si nastaviÅĨ vaÅĄu inÅĄtanciu s niekoÄžkÃŊmi beÅžnÃŊmi nastaveniami.", "onboarding_theme_description": "Vyberte farbu tÊmy pre vÃĄÅĄ server. MôŞete to aj neskôr zmeniÅĨ vo vaÅĄich nastaveniach.", "onboarding_user_welcome_description": "Začnime!", "onboarding_welcome_user": "Vitaj, {user}", @@ -1225,6 +1368,7 @@ "original": "originÃĄl", "other": "OstatnÊ", "other_devices": "ĎalÅĄie zariadenia", + "other_entities": "OstatnÊ subjekty", "other_variables": "OstatnÊ premennÊ", "owned": "VlastnenÊ", "owner": "Vlastník", @@ -1247,9 +1391,9 @@ "password_required": "Heslo je povinnÊ", "password_reset_success": "Obnovenie hesla ÃēspeÅĄnÊ", "past_durations": { - "days": "{days, plural, one {PoslednÃŊ deň} other {PoslednÃŊch # dní }}", - "hours": "{hours, plural, one {PoslednÃĄ hodina} other {PoslednÃŊch # hodín}}", - "years": "{years, plural, one {PoslednÃŊ rok} other {PoslednÊ # roky}}" + "days": "{days, plural, one {PoslednÃŊ deň} few {PoslednÊ # dni} other {PoslednÃŊch # dní }}", + "hours": "{hours, plural, one {PoslednÃĄ hodina} few {PoslednÊ # hodiny} other {PoslednÃŊch # hodín}}", + "years": "{years, plural, one {PoslednÃŊ rok} few {PoslednÊ # roky} other {PoslednÃŊch # rokov}}" }, "path": "Cesta", "pattern": "Vzor", @@ -1258,17 +1402,18 @@ "paused": "PozastavenÊ", "pending": "ČakajÃēce", "people": "ÄŊudia", - "people_edits_count": "{count, plural, one {UpravenÃĄ # osoba} other {UpravenÃŊch # Äžudí}}", + "people_edits_count": "{count, plural, one {UpravenÃĄ # osoba} few {UpravenÊ # osoby} other {UpravenÃŊch # osôb}}", "people_feature_description": "Prehliadanie fotiek a videí zoskupenÃŊch podÄža Äžudí", - "people_sidebar_description": "Zobrazí odkaz na ÄŊudí v bočnom paneli", + "people_sidebar_description": "ZobraziÅĨ odkaz na ÄŊudí v bočnom paneli", "permanent_deletion_warning": "Varovanie o trvalom zmazaní", "permanent_deletion_warning_setting_description": "ZobraziÅĨ varovanie pri trvalom zmazaní poloÅžky", "permanently_delete": "Trvalo zmazaÅĨ", - "permanently_delete_assets_count": "NavÅždy zmazaÅĨ {count, plural, one {poloÅžku} other {poloÅžky}}", - "permanently_delete_assets_prompt": "Naozaj si prajete navÅždy zmazaÅĨ {count, plural, one {tÃēto poloÅžku?} other {tÃŊchto # poloÅžiek?}} VymaÅžÃē sa aj {count, plural, one {zo svojho albumu} other {zo svojich albumov}}.", + "permanently_delete_assets_count": "Natrvalo vymazaÅĨ {count, plural, one {poloÅžku} few {poloÅžky} other {poloÅžiek}}", + "permanently_delete_assets_prompt": "Ste si istí, Åže chcete natrvalo vymazaÅĨ {count, plural, one {tÃēto poloÅžku?} few {tieto # poloÅžky?} other {tÃŊchto # poloÅžiek?}} TÃŊmto sa odstrÃĄni aj {count, plural, one {z jej albumu} other {zo svojich albumov}}.", "permanently_deleted_asset": "NavÅždy odstrÃĄnenÃĄ poloÅžka", - "permanently_deleted_assets_count": "NavÅždy {count, plural, one {odstrÃĄnenÃĄ # poloÅžka} other {odstrÃĄnenÊ # poloÅžky}}", + "permanently_deleted_assets_count": "Natrvalo {count, plural, one {odstrÃĄnenÃĄ # poloÅžka} few {odstrÃĄnenÊ # poloÅžky} other {odstrÃĄnenÃŊch # poloÅžiek}}", "permission": "Povolenie", + "permission_empty": "VaÅĄe povolenie by nemalo byÅĨ prÃĄzdne", "permission_onboarding_back": "SpäÅĨ", "permission_onboarding_continue_anyway": "PokračovaÅĨ aj tak", "permission_onboarding_get_started": "ZačaÅĨ", @@ -1283,9 +1428,13 @@ "photo_shared_all_users": "VyzerÃĄ, Åže zdieÄžate svoje fotky so vÅĄetkÃŊmi pouŞívateÄžmi alebo nemÃĄte Åžiadnych pouŞívateÄžov.", "photos": "Fotografie", "photos_and_videos": "Fotografie & Videa", - "photos_count": "{count, plural, one {{count, number} Fotka} other {{count, number} Fotiek}}", + "photos_count": "{count, plural, one {{count, number} fotka} few {{count, number} fotky} other {{count, number} fotiek}}", "photos_from_previous_years": "Fotky z minulÃŊch rokov", "pick_a_location": "Vyberte polohu", + "pin_code_changed_successfully": "ÚspeÅĄne ste zmenili PIN kÃŗd", + "pin_code_reset_successfully": "ÚspeÅĄne ste obnovili PIN kÃŗd", + "pin_code_setup_successfully": "ÚspeÅĄne ste nastavili PIN kÃŗd", + "pin_verification": "Overenie PIN kÃŗdom", "place": "Miesto", "places": "Miesta", "places_count": "{count, plural, one {{count, number} miesto} few {{count, number} miesta} other {{count, number} miest}}", @@ -1293,17 +1442,22 @@ "play_memories": "PrehraÅĨ spomienky", "play_motion_photo": "PrehraÅĨ pohyblivÃē fotku", "play_or_pause_video": "Pustí alebo pozastaví video", + "please_auth_to_access": "Prosím, potvrďte overenie pre prístup", "port": "Port", - "preferences_settings_title": "Preferencie", - "preset": "Prednastavenie", + "preferences_settings_subtitle": "SpravovaÅĨ predvoÄžby aplikÃĄcie", + "preferences_settings_title": "PredvoÄžby", + "preset": "PredvoÄžba", "preview": "NÃĄhÄžad", "previous": "PredoÅĄlÊ", "previous_memory": "PredoÅĄlÃĄ spomienka", + "previous_or_next_day": "Deň dopredu/dozadu", + "previous_or_next_month": "Mesiac dopredu/dozadu", "previous_or_next_photo": "Fotka ďalÅĄia/predoÅĄlÃĄ", + "previous_or_next_year": "Rok dopredu/dozadu", "primary": "PrimÃĄrne", "privacy": "SÃēkromie", "profile": "Profil", - "profile_drawer_app_logs": "Logy", + "profile_drawer_app_logs": "ZÃĄznamy", "profile_drawer_client_out_of_date_major": "MobilnÃĄ aplikÃĄcia je zastaralÃĄ. Prosím aktualizujte na najnovÅĄiu verziu.", "profile_drawer_client_out_of_date_minor": "MobilnÃĄ aplikÃĄcia je zastaralÃĄ. Prosím aktualizujte na najnovÅĄiu verziu.", "profile_drawer_client_server_up_to_date": "Klient a server sÃē aktuÃĄlne", @@ -1327,7 +1481,7 @@ "purchase_button_select": "VybraÅĨ", "purchase_failed_activation": "AktivÃĄcia sa nepodarila! Prosím skontrolujte email či je sprÃĄvny kÄžÃēč produktu!", "purchase_individual_description_1": "Pre jednotlivca", - "purchase_individual_description_2": "Stav podporovateÄža", + "purchase_individual_description_2": "Å tatÃēt podporovateÄža", "purchase_individual_title": "Jednotlivec", "purchase_input_suggestion": "MÃĄte produktovÃŊ kÄžÃēč? Zadajte ho niÅžÅĄie", "purchase_license_subtitle": "KÃēpte si Immich a podporte neustÃĄly vÃŊvoj tejto sluÅžby", @@ -1343,46 +1497,52 @@ "purchase_remove_server_product_key": "OdstrÃĄniÅĨ produktovÃŊ kÄžÃēč servera", "purchase_remove_server_product_key_prompt": "Naozaj chcete odstrÃĄniÅĨ produktovÃŊ kÄžÃēč servera?", "purchase_server_description_1": "Pre celÃŊ server", - "purchase_server_description_2": "Stav podporovateÄža", + "purchase_server_description_2": "Å tatÃēt podporovateÄža", "purchase_server_title": "Server", "purchase_settings_server_activated": "ProduktovÃŊ kÄžÃēč servera spravuje admin", + "queue_status": "V poradí {count}/{total}", "rating": "Hodnotenie hviezdičkami", "rating_clear": "VyčistiÅĨ hodnotenie", - "rating_count": "{count, plural, one {# hviezdička} other {# hviezdičky}}", - "rating_description": "Zobrazí EXIF hodnotenie v info paneli", + "rating_count": "{count, plural, one {# hviezdička} few {# hviezdičky} other {# hviezdičiek}}", + "rating_description": "ZobraziÅĨ EXIF hodnotenie v informačnom paneli", "reaction_options": "MoÅžnosti reakcie", "read_changelog": "PrečítaÅĨ zoznam zmien", "reassign": "PreradiÅĨ", - "reassigned_assets_to_existing_person": "PreradenÊ {count, plural, one {# poloÅžka} other {# poloÅžky}} k {name, select, null {existujÃēcej osobe} other {{name}}}", - "reassigned_assets_to_new_person": "PreradenÊ {count, plural, one {# poloÅžka} other {# poloÅžiek}} novej osobe", + "reassigned_assets_to_existing_person": "Opätovne {count, plural, one {priradenÃĄ # poloÅžka} few {priradenÊ # poloÅžky} other {priradenÃŊch # poloÅžiek}} k {name, select, null {existujÃēcej osobe} other {{name}}}", + "reassigned_assets_to_new_person": "Opätovne {count, plural, one {priradenÃĄ # poloÅžka} few {priradenÊ # poloÅžky} other {priradenÃŊch # poloÅžiek}} novej osobe", "reassing_hint": "Priradí zvolenÃē poloÅžku k existujÃēcej osobe", "recent": "NedÃĄvne", "recent-albums": "PoslednÊ albumy", "recent_searches": "PoslednÊ vyhÄžadÃĄvania", + "recently_added": "NedÃĄvno pridanÊ", "recently_added_page_title": "NedÃĄvno pridanÊ", "recently_taken": "NedÃĄvno nasnímanÊ", "recently_taken_page_title": "NedÃĄvno zhotovenÊ", - "refresh": "ObnoviÅĨ", + "refresh": "AktualizovaÅĨ", "refresh_encoded_videos": "ObnoviÅĨ enkÃŗdovanÊ videÃĄ", "refresh_faces": "ObnoviÅĨ tvÃĄre", "refresh_metadata": "ObnoviÅĨ metadÃĄta", "refresh_thumbnails": "ObnoviÅĨ miniatÃēry", - "refreshed": "AktualizovanÊ", + "refreshed": "ObnovenÊ", "refreshes_every_file": "Znova prečíta vÅĄetky existujÃēce a novÊ sÃēbory", "refreshing_encoded_video": "Obnovovanie enkÃŗdovanÃŊch videí", - "refreshing_faces": "Obnovovnie tvÃĄrí", + "refreshing_faces": "Obnovovanie tvÃĄrí", "refreshing_metadata": "Obnovovanie metadÃĄt", "regenerating_thumbnails": "Pregenerovanie nÃĄhÄžadov", + "remote": "VzdialenÊ", + "remote_assets": "VzdialenÊ poloÅžky", "remove": "OdstrÃĄniÅĨ", - "remove_assets_album_confirmation": "Naozaj chcete odstrÃĄniÅĨ {count, plural, one {# poloÅžky} other {# poloÅžiek}} z albumu?", - "remove_assets_shared_link_confirmation": "Naozaj chcete odstrÃĄniÅĨ {count, plural, one {# poloÅžku} other {# poloÅžiek}} z tohoto zdieÄžanÊho odkazu?", + "remove_assets_album_confirmation": "Naozaj chcete odstrÃĄniÅĨ {count, plural, one {# poloÅžku} few {# poloÅžky} other {# poloÅžiek}} z albumu?", + "remove_assets_shared_link_confirmation": "Naozaj chcete odstrÃĄniÅĨ {count, plural, one {# poloÅžku} few {# poloÅžky} other {# poloÅžiek}} z tohoto zdieÄžanÊho odkazu?", "remove_assets_title": "OdstrÃĄniÅĨ poloÅžky?", "remove_custom_date_range": "OdstrÃĄniÅĨ vlastnÃŊ rozsah dÃĄtumov", "remove_deleted_assets": "OdstrÃĄniÅĨ vymazanÊ poloÅžky", "remove_from_album": "OdstrÃĄniÅĨ z albumu", + "remove_from_album_action_prompt": "{count} odstrÃĄnenÊ z albumu", "remove_from_favorites": "OdstrÃĄniÅĨ z obÄžÃēbenÃŊch", - "remove_from_locked_folder": "OdstrÃĄniÅĨ zo zamknutÊho priečinka", - "remove_from_locked_folder_confirmation": "Ste si istí, Åže chcete tieto fotografie a videÃĄ presunÃēÅĨ zo zamknutÊho priečinka? BudÃē viditeÄžnÊ vo vaÅĄej kniÅžnici.", + "remove_from_lock_folder_action_prompt": "{count} odobranÊ zo zamknutÊho priečinka", + "remove_from_locked_folder": "OdobraÅĨ zo zamknutÊho priečinka", + "remove_from_locked_folder_confirmation": "Ste si istí, Åže chcete tieto fotografie a videÃĄ odobraÅĨ zo zamknutÊho priečinka? BudÃē viditeÄžnÊ vo vaÅĄej kniÅžnici.", "remove_from_shared_link": "OdstrÃĄniÅĨ zo zdieÄžanÊho odkazu", "remove_memory": "OdstrÃĄniÅĨ spomienku", "remove_photo_from_memory": "OdstrÃĄniÅĨ fotografiu z tejto spomienky", @@ -1395,7 +1555,7 @@ "removed_from_favorites_count": "{count, plural, other {OdstrÃĄnenÃŊch #}} z obÄžÃēbenÃŊch", "removed_memory": "OdstrÃĄnenÃĄ pamäÅĨ", "removed_photo_from_memory": "Fotografia odstrÃĄnenÃĄ z pamäte", - "removed_tagged_assets": "OdstrÃĄnenÃĄ značka z {count, plural, one {# poloÅžky} other {# poloÅžiek}}", + "removed_tagged_assets": "OdstrÃĄnenÃŊ ÅĄtítok z {count, plural, one {# poloÅžky} other {# poloÅžiek}}", "rename": "PremenovaÅĨ", "repair": "OpraviÅĨ", "repair_no_results_message": "NesledovanÊ a chÃŊbajÃēce sÃēbory sa zobrazia tu", @@ -1404,27 +1564,33 @@ "require_password": "VyÅžadovaÅĨ heslo", "require_user_to_change_password_on_first_login": "VyÅžadovaÅĨ zmenu hesla po prvom prihlÃĄsení", "rescan": "OpätovnÊ vyhÄžadÃĄvanie", - "reset": "ResetovaÅĨ", + "reset": "ObnoviÅĨ", "reset_password": "ObnoviÅĨ heslo", - "reset_people_visibility": "ResetovaÅĨ viditeÄžnosÅĨ Äžudí", + "reset_people_visibility": "ObnoviÅĨ viditeÄžnosÅĨ Äžudí", "reset_pin_code": "ObnoviÅĨ PIN kÃŗd", - "reset_to_default": "ResetovaÅĨ na predvolenÊ", + "reset_sqlite": "ObnoviÅĨ SQLite databÃĄzu", + "reset_sqlite_confirmation": "Ste si istí, Åže chcete obnoviÅĨ SQLite databÃĄzu? Na opätovnÃē synchronizÃĄciu Ãēdajov sa budete musieÅĨ odhlÃĄsiÅĨ a znova prihlÃĄsiÅĨ", + "reset_sqlite_success": "ÚspeÅĄnÊ obnovenie databÃĄzy SQLite", + "reset_to_default": "ObnoviÅĨ na predvolenÊ", "resolve_duplicates": "VyrieÅĄiÅĨ duplicity", "resolved_all_duplicates": "VyrieÅĄenÊ vÅĄetky duplicity", "restore": "NavrÃĄtiÅĨ", "restore_all": "NavrÃĄtit vÅĄetko", + "restore_trash_action_prompt": "{count} obnovenÃŊch z koÅĄa", "restore_user": "NavrÃĄtiÅĨ pouŞívateÄža", "restored_asset": "NavrÃĄtenÊ poloÅžky", "resume": "PokračovaÅĨ", "retry_upload": "ZopakovaÅĨ nahrÃĄvanie", - "review_duplicates": "PrezrieÅĨ duplikÃĄty", + "review_duplicates": "PreskÃēmaÅĨ duplikÃĄty", "role": "Rola", "role_editor": "Editor", "role_viewer": "DivÃĄk", + "running": "SpustenÊ", "save": "UloÅžiÅĨ", + "save_to_gallery": "UloÅžiÅĨ do galÊrie", "saved_api_key": "UloÅženÃŊ API KÄžÃēč", "saved_profile": "UloÅženÃŊ profil", - "saved_settings": "UloÅženÊ nastavenia", + "saved_settings": "Nastavenia boli uloÅženÊ", "say_something": "NapÃ­ÅĄte niečo", "scaffold_body_error_occurred": "Vyskytla sa chyba", "scan_all_libraries": "PreskenovaÅĨ vÅĄetky kniÅžnice", @@ -1436,7 +1602,7 @@ "search_by_context": "HÄžadaÅĨ s kontextom", "search_by_description": "VyhÄžadÃĄvanie podÄža popisu", "search_by_description_example": "PeÅĄia turistika v Sape", - "search_by_filename": "HÄžadaÅĨ s nÃĄzvom alebo príponou sÃēboru", + "search_by_filename": "HÄžadaÅĨ podÄža nÃĄzvu alebo prípony sÃēboru", "search_by_filename_example": "napr. IMG_1234.JPG alebo PNG", "search_camera_make": "HÄžadaÅĨ značku fotoaparÃĄtu...", "search_camera_model": "HÄžadaÅĨ model fotoaparÃĄtu...", @@ -1445,9 +1611,11 @@ "search_filter_apply": "PouÅžiÅĨ filter", "search_filter_camera_title": "Vyberte typ kamery", "search_filter_date": "DÃĄtum", + "search_filter_date_interval": "{start} do {end}", "search_filter_date_title": "Vyberte rozsah dÃĄtumov", "search_filter_display_option_not_in_album": "Mimo albumu", "search_filter_display_options": "MoÅžnosti zobrazenia", + "search_filter_filename": "HÄžadaÅĨ podÄža nÃĄzvu sÃēboru", "search_filter_location": "Poloha", "search_filter_location_title": "Vyberte polohu", "search_filter_media_type": "Typ mÊdia", @@ -1490,6 +1658,7 @@ "select_album_cover": "Vyberte obal albumu", "select_all": "VybraÅĨ vÅĄetko", "select_all_duplicates": "VybraÅĨ vÅĄetky duplikÃĄty", + "select_all_in": "OznačiÅĨ vÅĄetky v {group}", "select_avatar_color": "Vyberte farbu avatara", "select_face": "Vyberte tvÃĄr", "select_featured_photo": "Vyberte nÃĄhÄžadovÃē fotku", @@ -1502,15 +1671,17 @@ "select_trash_all": "VybraÅĨ zahodiÅĨ vÅĄetky", "select_user_for_sharing_page_err_album": "Nepodarilo sa vytvoriÅĨ album", "selected": "VybranÊ", - "selected_count": "{count, plural, other {# vybranÊ}}", + "selected_count": "{count, plural, one {# vybranÃĄ} few {# vybranÊ} other {# vybranÃŊch}}", "send_message": "OdoslaÅĨ sprÃĄvu", "send_welcome_email": "OdoslaÅĨ uvítací e-mail", + "server_endpoint": "KoncovÃŊ bod servera", "server_info_box_app_version": "Verzia aplikÃĄcie", - "server_info_box_server_url": "URL Serveru", + "server_info_box_server_url": "URL adresa servera", "server_offline": "Server je Offline", "server_online": "Server je Online", - "server_stats": "ServerovÊ Å tatistiky", - "server_version": "Verzia Servera", + "server_privacy": "ZÃĄsady ochrany osobnÃŊch Ãēdajov servera", + "server_stats": "Å tatistiky servera", + "server_version": "Verzia servera", "set": "NastaviÅĨ", "set_as_album_cover": "NastaviÅĨ ako obal albumu", "set_as_featured_photo": "NastaviÅĨ ako hlavnÃē fotku", @@ -1518,14 +1689,16 @@ "set_date_of_birth": "NastaviÅĨ dÃĄtum narodenia", "set_profile_picture": "NastaviÅĨ profilovÃŊ obrÃĄzok", "set_slideshow_to_fullscreen": "NastaviÅĨ prezentÃĄciu na celÃē obrazovku", - "setting_image_viewer_help": "Prehliadač detailov najprv načíta malÃē miniatÃēru, potom načíta nÃĄhÄžad strednej veÄžkosti (ak je povolenÃŊ) a nakoniec načíta originÃĄl (ak je povolenÃŊ).", + "set_stack_primary_asset": "NastaviÅĨ ako primÃĄrnu poloÅžku", + "setting_image_viewer_help": "V detailnom prehliadači sa najprv načíta malÃĄ miniatÃēra, potom sa načíta stredne veÄžkÃŊ nÃĄhÄžad (ak je povolenÃŊ) a nakoniec sa načíta originÃĄl (ak je povolenÃŊ).", "setting_image_viewer_original_subtitle": "Povolením umoÅžníte načítaÅĨ pôvodnÃŊ obrÃĄzok v plnom rozlÃ­ÅĄení (veÄžkÃŊ!). ZakÃĄzaním zníŞite pouŞívania dÃĄt (v sieti, aj v dočasnej pamäte zariadenia).", "setting_image_viewer_original_title": "NačítaÅĨ pôvodnÃŊ obrÃĄzok", "setting_image_viewer_preview_subtitle": "Povolením umoÅžníte načítaÅĨ obrÃĄzok so strednÃŊm rozlÃ­ÅĄením. ZakÃĄÅžte, ak chcete priamo načítaÅĨ originÃĄl alebo pouÅžiÅĨ iba miniatÃēru.", "setting_image_viewer_preview_title": "NačítaÅĨ nÃĄhÄžad obrÃĄzka", "setting_image_viewer_title": "ObrÃĄzky", "setting_languages_apply": "PouÅžiÅĨ", - "setting_notifications_notify_failures_grace_period": "OznÃĄmenie o zlyhaní zÃĄlohovania na pozadí: {duration}", + "setting_languages_subtitle": "ZmeniÅĨ jazyk aplikÃĄcie", + "setting_notifications_notify_failures_grace_period": "UpozorniÅĨ na zlyhanie zÃĄlohovania na pozadí: {duration}", "setting_notifications_notify_hours": "{count} hodín", "setting_notifications_notify_immediately": "okamÅžite", "setting_notifications_notify_minutes": "{count} minÃēt", @@ -1533,14 +1706,18 @@ "setting_notifications_notify_seconds": "{count} sekÃēnd", "setting_notifications_single_progress_subtitle": "PodrobnÊ informÃĄcie o priebehu nahrÃĄvania pre poloÅžku", "setting_notifications_single_progress_title": "ZobraziÅĨ priebeh detailov zÃĄlohovania na pozadí", - "setting_notifications_subtitle": "Prispôsobenie predvolieb oznÃĄmení", + "setting_notifications_subtitle": "Upravte svoje nastavenia oznÃĄmení", "setting_notifications_total_progress_subtitle": "CelkovÃŊ priebeh nahrÃĄvania (nahranÃŊch/celkovo)", "setting_notifications_total_progress_title": "ZobraziÅĨ celkovÃŊ priebeh zÃĄlohovania na pozadí", "setting_video_viewer_looping_title": "Opakovanie", + "setting_video_viewer_original_video_subtitle": "Pri streamovaní videa zo servera prehraÅĨ originÃĄl, aj keď je k dispozícii prekÃŗdovanÊ video. MôŞe to viesÅĨ k preruÅĄovanÊmu prehrÃĄvaniu videa. VideÃĄ dostupnÊ lokÃĄlne sa prehrajÃē v pôvodnej kvalite bez ohÄžadu na toto nastavenie.", + "setting_video_viewer_original_video_title": "VynÃētiÅĨ pôvodnÊ video", "settings": "Nastavenia", "settings_require_restart": "Na pouÅžitie tohto nastavenia reÅĄtartujte Immich", "settings_saved": "Nastavenia boli uloÅženÊ", + "setup_pin_code": "Nastavte si PIN kÃŗd", "share": "ZdieÄžaÅĨ", + "share_action_prompt": "{count} poloÅžiek zdieÄžanÃŊch", "share_add_photos": "PridaÅĨ fotografie", "share_assets_selected": "{count} označenÃŊch", "share_dialog_preparing": "Pripravujem...", @@ -1574,14 +1751,14 @@ "shared_link_edit_password_hint": "Zadajte heslo zdieÄžania", "shared_link_edit_submit_button": "AktualizovaÅĨ odkaz", "shared_link_error_server_url_fetch": "NemoÅžno nÃĄjsÅĨ URL severa", - "shared_link_expires_day": "VyprÅĄÃ­ o {count} dní", + "shared_link_expires_day": "VyprÅĄÃ­ o {count} deň", "shared_link_expires_days": "VyprÅĄÃ­ o {count} dní", - "shared_link_expires_hour": "VyprÅĄÃ­ o {count} hodín", + "shared_link_expires_hour": "VyprÅĄÃ­ o {count} hodinu", "shared_link_expires_hours": "VyprÅĄÃ­ o {count} hodín", - "shared_link_expires_minute": "VyprÅĄÃ­ o {count} minÃēt", + "shared_link_expires_minute": "VyprÅĄÃ­ o {count} minÃētu", "shared_link_expires_minutes": "VyprÅĄÃ­ o {count} minÃēt", "shared_link_expires_never": "NevyprÅĄÃ­", - "shared_link_expires_second": "VyprÅĄÃ­ o {count} sekÃēnd", + "shared_link_expires_second": "VyprÅĄÃ­ o {count} sekundu", "shared_link_expires_seconds": "VyprÅĄÃ­ o {count} sekÃēnd", "shared_link_individual_shared": "IndividuÃĄlne zdieÄžanÊ", "shared_link_info_chip_metadata": "EXIF", @@ -1589,7 +1766,8 @@ "shared_link_options": "MoÅžnosti zdieÄžanÃŊch odkazov", "shared_links": "ZdieÄžanÊ odkazy", "shared_links_description": "ZdieÄžanie fotografií a videí pomocou odkazu", - "shared_photos_and_videos_count": "{assetCount, plural, other {# zdieÄžanÊ fotky a videÃĄ.}}", + "shared_photos_and_videos_count": "{assetCount, plural, few {# zdieÄžanÊ fotky a videÃĄ.} other {# zdieÄžanÃŊch fotiek a videí.}}", + "shared_with_me": "ZdieÄžanÊ so mnou", "shared_with_partner": "ZdieÄžanÊ s {partner}", "sharing": "ZdieÄžanie", "sharing_enter_password": "Ak chcete zobraziÅĨ tÃēto strÃĄnku, prosím, zadajte heslo.", @@ -1599,7 +1777,7 @@ "sharing_sidebar_description": "ZobraziÅĨ odkaz na ZdieÄžanie v bočnom paneli", "sharing_silver_appbar_create_shared_album": "VytvoriÅĨ zdieÄžanÃŊ album", "sharing_silver_appbar_share_partner": "ZdieÄžaÅĨ s partnerom", - "shift_to_permanent_delete": "stlačte ⇧ pre nemennÊ zmazanie ploÅžiek", + "shift_to_permanent_delete": "stlačte ⇧ na trvalÊ vymazanie poloÅžky", "show_album_options": "ZobraziÅĨ moÅžnosti albumu", "show_albums": "ZobraziÅĨ albumy", "show_all_people": "ZobraziÅĨ vÅĄetkÃŊch Äžudí", @@ -1608,21 +1786,21 @@ "show_gallery": "ZobraziÅĨ galÊriu", "show_hidden_people": "ZobraziÅĨ skrytÃŊch Äžudí", "show_in_timeline": "ZobraziÅĨ na časovej osi", - "show_in_timeline_setting_description": "Zobrazí fotky a videÃĄ tohoto pouŞívateÄža na časovej osi", + "show_in_timeline_setting_description": "ZobraziÅĨ fotky a videÃĄ tohoto pouŞívateÄža na vaÅĄej časovej osi", "show_keyboard_shortcuts": "ZobraziÅĨ klÃĄvesovÊ skratky", "show_metadata": "ZobraziÅĨ metadÃĄta", - "show_or_hide_info": "Zobrazí alebo skryje info", + "show_or_hide_info": "ZobraziÅĨ alebo skryÅĨ informÃĄcie", "show_password": "ZobraziÅĨ heslo", - "show_person_options": "Zobrazí moÅžnosti osoby", - "show_progress_bar": "Zobrazí ukazovateÄž priebehu", + "show_person_options": "ZobraziÅĨ moÅžnosti osoby", + "show_progress_bar": "ZobraziÅĨ ukazovateÄž priebehu", "show_search_options": "ZobraziÅĨ moÅžnosti vyhÄžadÃĄvania", "show_shared_links": "ZobraziÅĨ zdieÄžanÊ odkazy", - "show_slideshow_transition": "Zobrazí prechody v prezentÃĄcii", + "show_slideshow_transition": "ZobraziÅĨ prechody v prezentÃĄcii", "show_supporter_badge": "Odznak podporovateÄža", "show_supporter_badge_description": "ZobraziÅĨ odznak podporovateÄža", "shuffle": "NÃĄhodnÊ poradie", "sidebar": "BočnÃŊ panel", - "sidebar_display_description": "Zobrazí odkaz na pohÄžad v bočnom paneli", + "sidebar_display_description": "ZobraziÅĨ odkaz na zobrazenie v bočnom paneli", "sign_out": "OdhlÃĄsiÅĨ sa", "sign_up": "RegistrovaÅĨ", "size": "VeÄžkosÅĨ", @@ -1641,15 +1819,17 @@ "sort_title": "NÃĄzov", "source": "Zdroj", "stack": "Zoskupenie", + "stack_action_prompt": "{count} zoskupenÃŊch", "stack_duplicates": "ZoskupiÅĨ duplicity", "stack_select_one_photo": "Vyberte jednu hlavnÃē fotku pre zoskupenie", "stack_selected_photos": "ZoskupiÅĨ vybratÊ fotky", - "stacked_assets_count": "{count, plural, one {ZoskupenÃĄ # poloÅžka} other {ZoskupenÃŊch # poloÅžiek}}", + "stacked_assets_count": "{count, plural, one {ZoskupenÃĄ # poloÅžka} few {ZoskupenÊ # poloÅžky} other {ZoskupenÃŊch # poloÅžiek}}", "stacktrace": "VÃŊpis zÃĄsobníku", "start": "Å tart", - "start_date": "ZačiatočnÃŊ dÃĄtum", + "start_date": "PočiatočnÃŊ dÃĄtum", "state": "Å tÃĄt", "status": "Stav", + "stop_casting": "ZastaviÅĨ prenos", "stop_motion_photo": "Stopmotion fotka", "stop_photo_sharing": "ZastaviÅĨ zdieÄžanie vaÅĄich fotiek?", "stop_photo_sharing_description": "{partner} uÅž nebude maÅĨ prístup k vaÅĄim fotkÃĄm.", @@ -1659,6 +1839,7 @@ "storage_quota": "ÚloÅžnÃŊ limit", "storage_usage": "VyuÅžitÃŊch {used} z {available}", "submit": "OdoslaÅĨ", + "success": "Úspech", "suggestions": "NÃĄvrhy", "sunrise_on_the_beach": "VÃŊchod slnka na plÃĄÅži", "support": "Podpora", @@ -1667,24 +1848,34 @@ "swap_merge_direction": "VymeniÅĨ smer zlÃēčenia", "sync": "SynchronizovaÅĨ", "sync_albums": "SynchronizovaÅĨ albumy", - "tag": "Značka", - "tag_assets": "PridaÅĨ značku", - "tag_created": "VytvorenÃĄ značka: {tag}", - "tag_feature_description": "Prehliadanie fotiek a videÃĄ zoskupenÃŊch podÄža tematickÃŊch značiek", - "tag_not_found_question": "Neviete nÃĄjsÅĨ značku? Vytvorte novÃē značku.", + "sync_albums_manual_subtitle": "Synchronizujte vÅĄetky nahranÊ videÃĄ a fotografie s vybranÃŊmi zÃĄloÅžnÃŊmi albumami", + "sync_local": "SynchronizovaÅĨ lokÃĄlne", + "sync_remote": "SynchronizovaÅĨ vzdialenÊ", + "sync_upload_album_setting_subtitle": "VytvÃĄrajte a nahrÃĄvajte svoje fotografie a videÃĄ do vybranÃŊch albumov na Immich", + "tag": "Å títok", + "tag_assets": "PridaÅĨ ÅĄtítky", + "tag_created": "VytvorenÃŊ ÅĄtítok: {tag}", + "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í", - "tag_updated": "UpravenÃĄ značka: {tag}", - "tagged_assets": "Značka priradenÃĄ {count, plural, one {# poloÅžke} other {# poloÅžkÃĄm}}", + "tag_updated": "UpravenÃŊ ÅĄtítok: {tag}", + "tagged_assets": "Å títok priradenÃŊ {count, plural, one {# poloÅžke} other {# poloÅžkÃĄm}}", "tags": "Å títky", + "tap_to_run_job": "Ťuknutím na poloÅžku spustíte Ãēlohu", "template": "Å ablÃŗna", "theme": "TÊma", "theme_selection": "VÃŊber tÊmy", - "theme_selection_description": "Automaticky nastaví tÊmu na svetlÃē alebo tmavÃē podÄža systÊmovÃŊch preferencií v prehliadači", - "theme_setting_asset_list_storage_indicator_title": "ZobraziÅĨ indikÃĄtor ÃēloÅžiska na dlaÅždiciach poloÅžiek", + "theme_selection_description": "Automaticky nastaví tÊmu na svetlÃē alebo tmavÃē podÄža systÊmovÃŊch predvolieb v prehliadači", + "theme_setting_asset_list_storage_indicator_title": "ZobraziÅĨ indikÃĄtor ÃēloÅžiska na dlaÅždiciach mÊdií", "theme_setting_asset_list_tiles_per_row_title": "Počet poloÅžiek na riadok ({count})", - "theme_setting_image_viewer_quality_subtitle": "Prispôsobenie kvality prehliadača detailov", + "theme_setting_colorful_interface_subtitle": "PouÅžiÅĨ zÃĄkladnÃē farbu na plochy na pozadí.", + "theme_setting_colorful_interface_title": "FarebnÊ rozhranie", + "theme_setting_image_viewer_quality_subtitle": "Upravte kvalitu detailnÊho prehliadača obrÃĄzkov", "theme_setting_image_viewer_quality_title": "Kvalita prehliadača obrÃĄzkov", - "theme_setting_system_theme_switch": "Automaticky (podÄža systemovÊho nastavenia)", + "theme_setting_primary_color_subtitle": "Vyberte si farbu pre zÃĄkladnÊ akcie a dôrazy.", + "theme_setting_primary_color_title": "ZÃĄkladnÃĄ farba", + "theme_setting_system_primary_color_title": "PouÅžiÅĨ systÊmovÃē farbu", + "theme_setting_system_theme_switch": "Automaticky (podÄža systÊmovÊho nastavenia)", "theme_setting_theme_subtitle": "Vyberte nastavenia tÊmy aplikÃĄcie", "theme_setting_three_stage_loading_subtitle": "TrojstupňovÊ načítanie môŞe zvÃŊÅĄiÅĨ vÃŊkonnosÅĨ načítania, ale vedie k vÃŊrazne vyÅĄÅĄiemu zaÅĨaÅženiu siete", "theme_setting_three_stage_loading_title": "Povolenie trojstupňovÊho načítavania", @@ -1703,31 +1894,37 @@ "total": "Celkom", "total_usage": "CelkovÊ vyuÅžitie", "trash": "KÃ´ÅĄ", + "trash_action_prompt": "{count} presunutÃŊch do koÅĄa", "trash_all": "VÅĄetko do koÅĄa", "trash_count": "{count, number} do koÅĄa", "trash_delete_asset": "PoloÅžky do koÅĄa/odstrÃĄniÅĨ", + "trash_emptied": "KÃ´ÅĄ vyprÃĄzdnenÃŊ", "trash_no_results_message": "VymazanÊ fotografie a videÃĄ sa zobrazia tu.", "trash_page_delete_all": "VymazaÅĨ vÅĄetky", - "trash_page_empty_trash_dialog_content": "Skutočne chcete vyprÃĄzdniÅĨ kÃ´ÅĄ? Tieto poloÅžky budÃē permanentne odstrÃĄnenÊ z Immichu", + "trash_page_empty_trash_dialog_content": "Skutočne chcete vyprÃĄzdniÅĨ kÃ´ÅĄ? Tieto poloÅžky budÃē permanentne odstrÃĄnenÊ z aplikÃĄcie Immich", "trash_page_info": "MÊdiÃĄ v koÅĄi sa permanentne odstrÃĄnia po {days} dňoch", "trash_page_no_assets": "ÅŊiadne mÊdiÃĄ v koÅĄi", "trash_page_restore_all": "ObnoviÅĨ vÅĄetky", - "trash_page_select_assets_btn": "OznačiÅĨ mÊdiÃĄ", + "trash_page_select_assets_btn": "VybraÅĨ mÊdiÃĄ", "trash_page_title": "KÃ´ÅĄ ({count})", "trashed_items_will_be_permanently_deleted_after": "PoloÅžky v koÅĄi sa natrvalo vymaÅžÃē po {days, plural, one {# dni} other {# dňoch}}.", "type": "Typ", + "unable_to_change_pin_code": "Nie je moÅžnÊ zmeniÅĨ PIN kÃŗd", + "unable_to_setup_pin_code": "Nie je moÅžnÊ nastaviÅĨ PIN kÃŗd", "unarchive": "OdarchivovaÅĨ", + "unarchive_action_prompt": "{count} odstrÃĄnenÊ z archívu", "unarchived_count": "{count, plural, other {OdarchivovanÃŊch #}}", "undo": "SpäÅĨ", "unfavorite": "OdznačiÅĨ ako obÄžÃēbenÊ", + "unfavorite_action_prompt": "{count} odstrÃĄnenÊ z ObÄžÃēbenÃŊch", "unhide_person": "OdkryÅĨ osobu", "unknown": "NeznÃĄme", - "unknown_country": "NeznÃĄmy ÅĄtÃĄt", + "unknown_country": "NeznÃĄma krajina", "unknown_year": "NeznÃĄmy rok", "unlimited": "NeobmedzenÊ", "unlink_motion_video": "OdpojiÅĨ pohyblivÊ video", "unlink_oauth": "OdpojiÅĨ OAuth", - "unlinked_oauth_account": "OdpojiÅĨ OAuth Ãēčet", + "unlinked_oauth_account": "OdpojenÃŊ OAuth Ãēčet", "unmute_memories": "ZruÅĄenie stlmenia spomienok", "unnamed_album": "NepomenovanÃŊ album", "unnamed_album_delete_confirmation": "Ste si istÃŊ, Åže chcete zmazaÅĨ tento album?", @@ -1735,18 +1932,22 @@ "unsaved_change": "NeuloÅženÃĄ zmena", "unselect_all": "ZruÅĄiÅĨ vÃŊber vÅĄetkÃŊch", "unselect_all_duplicates": "ZruÅĄiÅĨ vÃŊber vÅĄetkÃŊch duplicít", + "unselect_all_in": "ZruÅĄiÅĨ vÃŊber vÅĄetkÃŊch v {group}", "unstack": "OdskupiÅĨ", - "unstacked_assets_count": "{count, plural, one {RozloÅženÃĄ # poloÅžka} few {RozloÅženÊ # poloÅžky} other {RozloÅženÃŊch # poloÅžiek}}", + "unstack_action_prompt": "{count} nezoskupenÃŊch", + "unstacked_assets_count": "ZruÅĄenie zoskupenia pre {count, plural, one {# poloÅžku} few {# poloÅžky} other {# poloÅžiek}}", + "untagged": "Bez ÅĄtítku", "up_next": "To je vÅĄetko", "updated_at": "AktualizovanÊ", "updated_password": "Heslo zmenenÊ", "upload": "NahraÅĨ", "upload_concurrency": "SÃēbeÅžnosÅĨ nahrÃĄvania", + "upload_details": "Podrobnosti o nahrÃĄvaní", "upload_dialog_info": "Chcete zÃĄlohovaÅĨ zvolenÊ mÊdiÃĄ na server?", "upload_dialog_title": "NahraÅĨ mÊdiÃĄ", - "upload_errors": "NahrÃĄvanie ukončenÊ s {count, plural, one {# chybou} other {# chybami}}, obnovte strÃĄnku aby sa zobrazili novÊ poloÅžky.", + "upload_errors": "NahrÃĄvanie ukončenÊ s {count, plural, one {# chybou} other {# chybami}}, obnovte strÃĄnku, aby sa zobrazili novÊ poloÅžky.", "upload_progress": "OstÃĄva {remaining, number} - SpracovanÃŊch {processed, number}/{total, number}", - "upload_skipped_duplicates": "{count, plural, one {PreskočenÃĄ # duplicita} few {PreskočenÊ # duplicity} other {PreskočenÃŊch # duplicít}}", + "upload_skipped_duplicates": "{count, plural, one {PreskočenÃĄ # duplicitnÃĄ poloÅžka} few {PreskočenÊ # duplicitnÊ poloÅžky} other {PreskočenÃŊch # duplicitnÃŊch poloÅžiek}}", "upload_status_duplicates": "DuplikÃĄty", "upload_status_errors": "Chyby", "upload_status_uploaded": "NahranÊ", @@ -1755,6 +1956,8 @@ "uploading": "NahrÃĄvanie", "url": "Odkaz URL", "usage": "PouÅžitie", + "use_biometric": "PouÅžiÅĨ biometrickÊ Ãēdaje", + "use_current_connection": "pouÅžiÅĨ aktuÃĄlne pripojenie", "use_custom_date_range": "PouÅžiÅĨ radÅĄej vlastnÃŊ rozsah dÃĄtumov", "user": "PouŞívateÄž", "user_has_been_deleted": "Tento pouŞívateÄž bol vymazanÃŊ.", @@ -1762,16 +1965,19 @@ "user_liked": "PouŞívateÄžovi {user} sa pÃĄÄi {type, select, photo {tÃĄto fotka} video {toto video} asset {tÃĄto poloÅžka} other {toto}}", "user_pin_code_settings": "PIN kÃŗd", "user_pin_code_settings_description": "Spravujte svoj PIN kÃŗd", + "user_privacy": "Ochrana osobnÃŊch Ãēdajov pouŞívateÄža", "user_purchase_settings": "NÃĄkup", - "user_purchase_settings_description": "SprÃĄva vÃĄÅĄho nÃĄkupu", + "user_purchase_settings_description": "Spravujte svoj nÃĄkup", "user_role_set": "Nastav {user} ako {role}", "user_usage_detail": "Podrobnosti o vyuŞívaní pouŞívateÄžmi", "user_usage_stats": "Å tatistiky vyuÅžitia Ãēčtu", "user_usage_stats_description": "ZobraziÅĨ ÅĄtatistiky vyuÅžitia Ãēčtu", "username": "PouŞívateÄžskÊ meno", "users": "PouŞívatelia", + "users_added_to_album_count": "{count, plural, one {PridanÃŊ # pouŞívateÄž} few {Pridaní # pouŞívatelia} other {PridanÃŊch # pouŞívateÄžov}} do albumu", "utilities": "NÃĄstroje", "validate": "ValidovaÅĨ", + "validate_endpoint_error": "Zadajte prosím platnÃē URL adresu", "variables": "PremennÊ", "version": "Verzia", "version_announcement_closing": "Tvoj kamarÃĄt, Alex", @@ -1783,10 +1989,11 @@ "video_hover_setting_description": "PrehrÃĄ video nÃĄhÄžad keď kurzor myÅĄi prejde cez poloÅžku. Aj keď je vypnutÊ, prehrÃĄvanie sa môŞe spustiÅĨ nabehnutí cez ikonu PrehraÅĨ.", "videos": "VideÃĄ", "videos_count": "{count, plural, one {# Video} few {# VideÃĄ} other {# Videí}}", - "view": "ZobraziÅĨ", + "view": "Zobrazenie", "view_album": "ZobraziÅĨ Album", "view_all": "ZobraziÅĨ vÅĄetky", "view_all_users": "ZobraziÅĨ vÅĄetkÃŊch pouŞívateÄžov", + "view_details": "ZobraziÅĨ podrobnosti", "view_in_timeline": "ZobraziÅĨ v časovej osi", "view_link": "ZobraziÅĨ odkaz", "view_links": "ZobraziÅĨ odkazy", @@ -1799,8 +2006,8 @@ "viewer_remove_from_stack": "OdstrÃĄniÅĨ zo zoskupenia", "viewer_stack_use_as_main_asset": "PouÅžiÅĨ ako hlavnÃē fotku", "viewer_unstack": "OdskupiÅĨ", - "visibility_changed": "ViditeÄžnosÅĨ zmenenÃĄ pre {count, plural, one {# osobu} other {# Äžudí}}", - "waiting": "ČakÃĄ", + "visibility_changed": "ViditeÄžnosÅĨ zmenenÃĄ pre {count, plural, one {# osobu} few {# osoby} other {# osôb}}", + "waiting": "ČakajÃēce", "warning": "Varovanie", "week": "TÃŊÅždeň", "welcome": "Vitajte", @@ -1810,7 +2017,7 @@ "year": "Rok", "years_ago": "pred {years, plural, one {# rokom} other {# rokmi}}", "yes": "Áno", - "you_dont_have_any_shared_links": "NemÃĄte Åžiadne zdielanÊ linky", + "you_dont_have_any_shared_links": "NemÃĄte Åžiadne zdielanÊ odkazy", "your_wifi_name": "VÃĄÅĄ nÃĄzov siete Wi-Fi", "zoom_image": "PriblíŞiÅĨ obrÃĄzok" } diff --git a/i18n/sl.json b/i18n/sl.json index 5132d09fc8..e08a9dd506 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -166,6 +166,20 @@ "metadata_settings_description": "Upravljanje nastavitev metapodatkov", "migration_job": "Migracija", "migration_job_description": "Preselite sličice za sredstva in obraze v najnovejÅĄo strukturo map", + "nightly_tasks_cluster_faces_setting_description": "ZaÅženi prepoznavanje obrazov na novo zaznanih obrazih", + "nightly_tasks_cluster_new_faces_setting": "ZdruÅžite nove obraze", + "nightly_tasks_database_cleanup_setting": "Naloge čiÅĄÄenja baze podatkov", + "nightly_tasks_database_cleanup_setting_description": "Očistite stare, potekle podatke iz baze podatkov", + "nightly_tasks_generate_memories_setting": "Ustvarjajte spomine", + "nightly_tasks_generate_memories_setting_description": "Ustvarite nove spomine iz sredstev", + "nightly_tasks_missing_thumbnails_setting": "Ustvari manjkajoče sličice", + "nightly_tasks_missing_thumbnails_setting_description": "Sredstva brez sličic postavite v čakalno vrsto za ustvarjanje sličic", + "nightly_tasks_settings": "Nastavitve nočnih opravil", + "nightly_tasks_settings_description": "Upravljajte nočne naloge", + "nightly_tasks_start_time_setting": "Začetni čas", + "nightly_tasks_start_time_setting_description": "Čas, ko streÅžnik začne izvajati nočne naloge", + "nightly_tasks_sync_quota_usage_setting": "Poraba kvote za sinhronizacijo", + "nightly_tasks_sync_quota_usage_setting_description": "Posodobi kvoto shrambe uporabnikov glede na trenutno uporabo", "no_paths_added": "Ni dodanih poti", "no_pattern_added": "Brez dodanega vzorca", "note_apply_storage_label_previous_assets": "Opomba: Če Åželite oznako za shranjevanje uporabiti za predhodno naloÅžena sredstva, zaÅženite", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "Mobilni preusmeritveni URI", "oauth_mobile_redirect_uri_override": "Preglasitev URI preusmeritve za mobilne naprave", "oauth_mobile_redirect_uri_override_description": "Omogoči, ko ponudnik OAuth ne dovoli mobilnega URI-ja, kot je ''{callback}''", + "oauth_role_claim": "Zahteva vloge", + "oauth_role_claim_description": "Samodejno dodeli skrbniÅĄki dostop na podlagi prisotnosti tega zahtevka. Zahtevek ima lahko ÂģuporabnikÂĢ ali ÂģskrbnikÂĢ.", "oauth_settings": "OAuth", "oauth_settings_description": "Upravljanje nastavitev prijave OAuth", "oauth_settings_more_details": "Za več podrobnosti o tej funkciji glejte dokumentacijo.", @@ -357,10 +373,12 @@ "admin_password": "SkrbniÅĄko geslo", "administration": "Administracija", "advanced": "Napredno", + "advanced_settings_beta_timeline_subtitle": "Preizkusite novo izkuÅĄnjo aplikacije", + "advanced_settings_beta_timeline_title": "Časovnica beta različice", "advanced_settings_enable_alternate_media_filter_subtitle": "Uporabite to moÅžnost za filtriranje medijev med sinhronizacijo na podlagi alternativnih meril. To poskusite le, če imate teÅžave z aplikacijo, ki zaznava vse albume.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTALNO] Uporabite alternativni filter za sinhronizacijo albuma v napravi", "advanced_settings_log_level_title": "Nivo dnevnika: {level}", - "advanced_settings_prefer_remote_subtitle": "Nekatere naprave zelo počasi nalagajo sličice iz sredstev v napravi. Aktivirajte to nastavitev, če Åželite namesto tega naloÅžiti oddaljene slike.", + "advanced_settings_prefer_remote_subtitle": "Nekatere naprave zelo počasi nalagajo sličice iz lokalnih sredstev. Aktivirajte to nastavitev, če Åželite namesto tega naloÅžiti oddaljene slike.", "advanced_settings_prefer_remote_title": "Uporabi raje oddaljene slike", "advanced_settings_proxy_headers_subtitle": "Določi proxy glavo, ki jo naj Immich poÅĄlje ob vsaki mreÅžni zahtevi", "advanced_settings_proxy_headers_title": "Proxy glave", @@ -388,6 +406,7 @@ "album_options": "MoÅžnosti albuma", "album_remove_user": "Odstrani uporabnika?", "album_remove_user_confirmation": "Ali ste prepričani, da Åželite odstraniti {user}?", + "album_search_not_found": "Ni najdenih albumov, ki bi ustrezali vaÅĄemu iskanju", "album_share_no_users": "Videti je, da ste ta album dali v skupno rabo z vsemi uporabniki ali pa nimate nobenega uporabnika, s katerim bi ga lahko delili.", "album_updated": "Album posodobljen", "album_updated_setting_description": "Prejmite e-poÅĄtno obvestilo, ko ima album v skupni rabi nova sredstva", @@ -407,6 +426,7 @@ "albums_default_sort_order": "Privzeti vrstni red razvrÅĄÄanja albumov", "albums_default_sort_order_description": "Začetni vrstni red razvrÅĄÄanja sredstev pri ustvarjanju novih albumov.", "albums_feature_description": "Zbirke sredstev, ki jih je mogoče deliti z drugimi uporabniki.", + "albums_on_device_count": "Albumi v napravi ({count})", "all": "Vse", "all_albums": "Vsi albumi", "all_people": "Vsi ljudje", @@ -427,6 +447,7 @@ "app_settings": "Nastavitve aplikacije", "appears_in": "Pojavi se v", "archive": "Arhiv", + "archive_action_prompt": "v arhiv je dodanih {count}", "archive_or_unarchive_photo": "Arhivirajte ali odstranite fotografijo iz arhiva", "archive_page_no_archived_assets": "Ni arhiviranih sredstev", "archive_page_title": "Arhiv ({count})", @@ -464,7 +485,6 @@ "assets": "Sredstva", "assets_added_count": "Dodano{count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", "assets_added_to_album_count": "Dodano {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v album", - "assets_added_to_name_count": "Dodano {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v {hasName, select, true {{name}} other {new album}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Sredstvo} two {Sredstvi} few {Sredstva} other {Sredstev}} ni mogoče dodati v album", "assets_count": "{count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", "assets_deleted_permanently": "trajno izrisana sredstva {count}", @@ -553,6 +573,8 @@ "backup_options_page_title": "MoÅžnosti varnostne kopije", "backup_setting_subtitle": "Upravljaj nastavitve nalaganja v ozadju in ospredju", "backward": "Nazaj", + "beta_sync": "Stanje sinhronizacije beta različice", + "beta_sync_subtitle": "Upravljanje novega sistema sinhronizacije", "biometric_auth_enabled": "Biometrična avtentikacija omogočena", "biometric_locked_out": "Biometrična avtentikacija vam je onemogočena", "biometric_no_options": "Biometrične moÅžnosti niso na voljo", @@ -570,7 +592,7 @@ "cache_settings_clear_cache_button": "Počisti predpomnilnik", "cache_settings_clear_cache_button_title": "Počisti predpomnilnik aplikacije. To bo znatno vplivalo na delovanje aplikacije, dokler se predpomnilnik ne obnovi.", "cache_settings_duplicated_assets_clear_button": "POČISTI", - "cache_settings_duplicated_assets_subtitle": "Fotografije in videoposnetki, ki jih je aplikacija uvrstila na črni seznam", + "cache_settings_duplicated_assets_subtitle": "Fotografije in videoposnetki, ki so prezrti s strani aplikacije", "cache_settings_duplicated_assets_title": "Podvojena sredstva ({count})", "cache_settings_statistics_album": "Sličice knjiÅžnice", "cache_settings_statistics_full": "Izvirne slike", @@ -587,6 +609,7 @@ "cancel": "Prekliči", "cancel_search": "Prekliči iskanje", "canceled": "Preklicano", + "canceling": "Preklic", "cannot_merge_people": "Oseb ni mogoče zdruÅžiti", "cannot_undo_this_action": "Tega dejanja ne morete razveljaviti!", "cannot_update_the_description": "Opisa ni mogoče posodobiti", @@ -703,7 +726,7 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Temno", - "darkTheme": "Preklopi na temno temo", + "dark_theme": "Preklopi temno temo", "date_after": "Datum po", "date_and_time": "Datum in ura", "date_before": "Datum pred", @@ -719,6 +742,7 @@ "default_locale": "Privzeti jezik", "default_locale_description": "Oblikujte datume in ÅĄtevilke glede na lokalne nastavitve brskalnika", "delete": "IzbriÅĄi", + "delete_action_prompt": "trajno izbrisano {count}", "delete_album": "IzbriÅĄi album", "delete_api_key_prompt": "Ali ste prepričani, da Åželite izbrisati ta API ključ?", "delete_dialog_alert": "Ti elementi bodo trajno izbrisani iz Immicha in vaÅĄe naprave", @@ -732,6 +756,7 @@ "delete_key": "IzbriÅĄi ključ", "delete_library": "IzbriÅĄi knjiÅžnico", "delete_link": "IzbriÅĄi povezavo", + "delete_local_action_prompt": "{count} izbrisano lokalno", "delete_local_dialog_ok_backed_up_only": "IzbriÅĄi samo kar je varnostno kopirano", "delete_local_dialog_ok_force": "Vseeno izbriÅĄi", "delete_others": "IzbriÅĄi ostale", @@ -745,6 +770,7 @@ "description": "Opis", "description_input_hint_text": "Dodaj opis ...", "description_input_submit_error": "Napaka pri posodabljanju opisa, preverite dnevnik za več podrobnosti", + "deselect_all": "Prekliči vse", "details": "Podrobnosti", "direction": "Usmeritev", "disabled": "Onemogočeno", @@ -762,6 +788,7 @@ "documentation": "Dokumentacija", "done": "Končano", "download": "Prenesi", + "download_action_prompt": "PrenaÅĄanje {count} sredstev", "download_canceled": "Prenos preklican", "download_complete": "Prenos končan", "download_enqueue": "Prenos v čakalni vrsti", @@ -799,6 +826,7 @@ "edit_key": "Uredi ključ", "edit_link": "Uredi povezavo", "edit_location": "Uredi lokacijo", + "edit_location_action_prompt": "urejenih {count} lokacij", "edit_location_dialog_title": "Lokacija", "edit_name": "Uredi ime", "edit_people": "Uredi osebe", @@ -817,6 +845,7 @@ "empty_trash": "Izprazni smetnjak", "empty_trash_confirmation": "Ste prepričani, da Åželite izprazniti smetnjak? S tem boste iz Immicha trajno odstranili vsa sredstva v smetnjaku.\nTega dejanja ne morete razveljaviti!", "enable": "Omogoči", + "enable_backup": "Omogoči varnostno kopiranje", "enable_biometric_auth_description": "Vnesite svojo PIN kodo, da omogočite biometrično preverjanje pristnosti", "enabled": "Omogočeno", "end_date": "Končni datum", @@ -984,6 +1013,7 @@ "failed_to_load_assets": "Sredstev ni bilo mogoče naloÅžiti", "failed_to_load_folder": "Mape ni bilo mogoče naloÅžiti", "favorite": "Priljubljen", + "favorite_action_prompt": "med priljubljene je dodanih {count}", "favorite_or_unfavorite_photo": "Priljubljena ali nepriljubljena fotografija", "favorites": "Priljubljene", "favorites_page_no_favorites": "Ni priljubljenih sredstev", @@ -1023,6 +1053,9 @@ "haptic_feedback_switch": "Uporabi haptičen odziv", "haptic_feedback_title": "Haptičen odziv", "has_quota": "Ima kvoto", + "hash_asset": "ZgoÅĄÄeno sredstvo", + "hashed_assets": "ZgoÅĄÄena sredstva", + "hashing": "ZgoÅĄÄevanje", "header_settings_add_header_tip": "Dodaj glavo", "header_settings_field_validator_msg": "Vrednost ne sme biti prazna", "header_settings_header_name_input": "Ime glave", @@ -1055,6 +1088,7 @@ "host": "Gostitelj", "hour": "Ura", "id": "ID", + "idle": "Nedejavnost", "ignore_icloud_photos": "Ignoriraj fotografije iCloud", "ignore_icloud_photos_description": "Fotografije, shranjene v iCloud, ne bodo naloÅžene na streÅžnik Immich", "image": "Slika", @@ -1127,6 +1161,7 @@ "library_page_sort_created": "Nazadnje ustvarjeno", "library_page_sort_last_modified": "Nazadnje spremenjeno", "library_page_sort_title": "Naslov albuma", + "licenses": "Licence", "light": "Svetlo", "like_deleted": "VÅĄeček izbrisan", "link_motion_video": "Povezava videa gibanja", @@ -1136,7 +1171,9 @@ "list": "Seznam", "loading": "Nalaganje", "loading_search_results_failed": "Nalaganje rezultatov iskanja ni uspelo", + "local": "Lokalno", "local_asset_cast_failed": "Sredstva, ki niso naloÅžena na streÅžnik, ni mogoče predvajati", + "local_assets": "Lokalna sredstva", "local_network": "Lokalno omreÅžje", "local_network_sheet_info": "Aplikacija se bo povezala s streÅžnikom prek tega URL-ja, ko bo uporabljala navedeno omreÅžje Wi-Fi", "location_permission": "Dovoljenje za lokacijo", @@ -1246,6 +1283,7 @@ "more": "Več", "move": "Premakni", "move_off_locked_folder": "Premakni iz zaklenjene mape", + "move_to_lock_folder_action_prompt": "V zaklenjeno mapo je bilo dodanih {count}", "move_to_locked_folder": "Premakni v zaklenjeno mapo", "move_to_locked_folder_confirmation": "Te fotografije in videoposnetki bodo odstranjeni iz vseh albumov in si jih bo mogoče ogledati le v zaklenjeni mapi", "moved_to_archive": "Premaknjeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v arhiv", @@ -1292,6 +1330,7 @@ "no_results": "Brez rezultatov", "no_results_description": "Poskusite s sinonimom ali bolj sploÅĄno ključno besedo", "no_shared_albums_message": "Ustvarite album za skupno rabo fotografij in videoposnetkov z osebami v vaÅĄem omreÅžju", + "no_uploads_in_progress": "Ni nalaganj v teku", "not_in_any_album": "Ni v nobenem albumu", "not_selected": "Ni izbrano", "note_apply_storage_label_to_previously_uploaded assets": "Opomba: Če Åželite oznako za shranjevanje uporabiti za predhodno naloÅžena sredstva, zaÅženite", @@ -1329,6 +1368,7 @@ "original": "izvirnik", "other": "drugo", "other_devices": "Druge naprave", + "other_entities": "Drugi subjekti", "other_variables": "Druge spremenljivke", "owned": "V lasti", "owner": "Lastnik", @@ -1360,7 +1400,7 @@ "pause": "Premor", "pause_memories": "Zaustavi spomine", "paused": "Zaustavljeno", - "pending": "V teku", + "pending": "Čakanje", "people": "Osebe", "people_edits_count": "{count, plural, one {Urejena # oseba} two {Urejeni # osebi} few {Urejene # osebe} other {Urejenih # oseb}}", "people_feature_description": "Brskanje po fotografijah in videoposnetkih, razvrÅĄÄenih po osebah", @@ -1460,6 +1500,7 @@ "purchase_server_description_2": "Status podpornika", "purchase_server_title": "StreÅžnik", "purchase_settings_server_activated": "Ključ izdelka streÅžnika upravlja skrbnik", + "queue_status": "Čakalna vrsta {count}/{total}", "rating": "Ocena z zvezdicami", "rating_clear": "Počisti oceno", "rating_count": "{count, plural, one {# zvezdica} two {# zvezdici} few {# zvezdice} other {# zvezdic}}", @@ -1488,6 +1529,8 @@ "refreshing_faces": "OsveÅževanje obrazev", "refreshing_metadata": "OsveÅževanje metapodatkov", "regenerating_thumbnails": "Obnavljanje sličic", + "remote": "Oddaljeno", + "remote_assets": "Oddaljena sredstva", "remove": "Odstrani", "remove_assets_album_confirmation": "Ali ste prepričani, da Åželite odstraniti {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} iz albuma?", "remove_assets_shared_link_confirmation": "Ali ste prepričani, da Åželite odstraniti {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} iz te skupne povezave?", @@ -1495,7 +1538,9 @@ "remove_custom_date_range": "Odstrani časovno obdobje po meri", "remove_deleted_assets": "Odstrani izbrisana sredstva", "remove_from_album": "Odstrani iz albuma", + "remove_from_album_action_prompt": "{count} odstranjenih iz albuma", "remove_from_favorites": "Odstrani iz priljubljenih", + "remove_from_lock_folder_action_prompt": "iz zaklenjene mape je odstranjenih {count}", "remove_from_locked_folder": "Odstrani iz zaklenjene mape", "remove_from_locked_folder_confirmation": "Ali ste prepričani, da Åželite premakniti te fotografije in videoposnetke iz zaklenjene mape? Vidni bodo v vaÅĄi knjiÅžnici.", "remove_from_shared_link": "Odstrani iz skupne povezave", @@ -1523,11 +1568,15 @@ "reset_password": "Ponastavi geslo", "reset_people_visibility": "Ponastavi vidnost ljudi", "reset_pin_code": "Ponastavi PIN kodo", + "reset_sqlite": "Ponastavi bazo podatkov SQLite", + "reset_sqlite_confirmation": "Ali ste prepričani, da Åželite ponastaviti bazo podatkov SQLite? Za ponovno sinhronizacijo podatkov se boste morali odjaviti in znova prijaviti", + "reset_sqlite_success": "UspeÅĄno ponastavljena baza podatkov SQLite", "reset_to_default": "Ponastavi na privzeto", "resolve_duplicates": "RazreÅĄi dvojnike", "resolved_all_duplicates": "RazreÅĄeni vsi dvojniki", "restore": "Obnovi", "restore_all": "Obnovi vse", + "restore_trash_action_prompt": "{count} obnovljenih iz koÅĄa", "restore_user": "Obnovi uporabnika", "restored_asset": "Obnovljeno sredstvo", "resume": "Nadaljuj", @@ -1536,6 +1585,7 @@ "role": "Vloga", "role_editor": "Urejevalec", "role_viewer": "Gledalec", + "running": "V teku", "save": "Shrani", "save_to_gallery": "Shrani v galerijo", "saved_api_key": "Shranjen API ključ", @@ -1667,6 +1717,7 @@ "settings_saved": "Nastavitve shranjene", "setup_pin_code": "Nastavi PIN kodo", "share": "Deli", + "share_action_prompt": "Deljena sredstva {count}", "share_add_photos": "Dodaj fotografije", "share_assets_selected": "{count} izbrano", "share_dialog_preparing": "Priprava...", @@ -1768,6 +1819,7 @@ "sort_title": "Naslov", "source": "Vir", "stack": "Sklad", + "stack_action_prompt": "{count} naloÅženih", "stack_duplicates": "Nabor dvojnikov", "stack_select_one_photo": "Izberite eno glavno fotografijo za nabor", "stack_selected_photos": "Nabor izbranih fotografij", @@ -1787,6 +1839,7 @@ "storage_quota": "Kvota shranjevanja", "storage_usage": "uporabljeno {used} od {available}", "submit": "PredloÅži", + "success": "Uspeh", "suggestions": "Predlogi", "sunrise_on_the_beach": "Sončni vzhod na plaÅži", "support": "Podpora", @@ -1796,6 +1849,8 @@ "sync": "Sinhronizacija", "sync_albums": "Sinhronizacija albumov", "sync_albums_manual_subtitle": "Sinhronizirajte vse naloÅžene videoposnetke in fotografije v izbrane varnostne albume", + "sync_local": "Sinhroniziraj lokalno", + "sync_remote": "Sinhroniziraj oddaljeno", "sync_upload_album_setting_subtitle": "Ustvarite in naloÅžite svoje fotografije in videoposnetke v izbrane albume na Immich", "tag": "Oznaka", "tag_assets": "Označi sredstva", @@ -1806,6 +1861,7 @@ "tag_updated": "Posodobljena oznaka: {tag}", "tagged_assets": "Označeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", "tags": "Oznake", + "tap_to_run_job": "Dotaknite se za zagon opravila", "template": "Predloga", "theme": "Tema", "theme_selection": "Izbira teme", @@ -1838,6 +1894,7 @@ "total": "Skupno", "total_usage": "Skupna poraba", "trash": "Smetnjak", + "trash_action_prompt": "premaknjeno v smetnjak {count}", "trash_all": "Vse v smetnjak", "trash_count": "Smetnjak {count, number}", "trash_delete_asset": "V smetnjak/izbriÅĄi sredstvo", @@ -1855,9 +1912,11 @@ "unable_to_change_pin_code": "PIN kode ni mogoče spremeniti", "unable_to_setup_pin_code": "PIN kode ni mogoče nastaviti", "unarchive": "Odstrani iz arhiva", + "unarchive_action_prompt": "{count} odstranjenih iz arhiva", "unarchived_count": "{count, plural, other {nearhiviranih #}}", "undo": "Razveljavi", "unfavorite": "Odznači priljubljeno", + "unfavorite_action_prompt": "{count} odstranjenih iz priljubljenih", "unhide_person": "PrikaÅži osebo", "unknown": "Neznano", "unknown_country": "Neznana drÅžava", @@ -1875,12 +1934,15 @@ "unselect_all_duplicates": "Odznači vse dvojnike", "unselect_all_in": "Prekliči izbor vseh v {group}", "unstack": "Razklad", + "unstack_action_prompt": "{count} razloÅženih", "unstacked_assets_count": "RazloÅži {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", + "untagged": "Neoznačeno", "up_next": "Naslednja", "updated_at": "Posodobljeno", "updated_password": "Posodobljeno geslo", "upload": "NaloÅži", "upload_concurrency": "Sočasnost nalaganja", + "upload_details": "Podrobnosti o nalaganju", "upload_dialog_info": "Ali Åželite varnostno kopirati izbrana sredstva na streÅžnik?", "upload_dialog_title": "NaloÅži sredstvo", "upload_errors": "Nalaganje je končano s/z {count, plural, one {# napako} two {# napakama} other {# napakami}}, osveÅžite stran, da vidite nova sredstva za nalaganje.", @@ -1912,6 +1974,7 @@ "user_usage_stats_description": "Oglejte si statistiko uporabe računa", "username": "UporabniÅĄko ime", "users": "Uporabniki", + "users_added_to_album_count": "V album {count, plural, one {je bil dodan # uporabnik} two {sta bila dodana # uporabnika} few {so bili dodani # uporabniki} other {je bilo dodanih # uporanikov}}", "utilities": "Pripomočki", "validate": "Potrdi", "validate_endpoint_error": "Vnesite veljaven URL", @@ -1930,6 +1993,7 @@ "view_album": "Ogled albuma", "view_all": "Poglej vse", "view_all_users": "Ogled vseh uporabnikov", + "view_details": "Ogled podrobnosti", "view_in_timeline": "Ogled na časovnici", "view_link": "Odpri povezavo", "view_links": "Ogled povezav", diff --git a/i18n/sr_Cyrl.json b/i18n/sr_Cyrl.json index b518bb39af..6216e671a8 100644 --- a/i18n/sr_Cyrl.json +++ b/i18n/sr_Cyrl.json @@ -458,7 +458,6 @@ "assets": "ЗаĐŋĐ¸ŅĐ¸", "assets_added_count": "Đ”ĐžĐ´Đ°Ņ‚Đž {count, plural, one {# Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēа} other {# Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēа}}", "assets_added_to_album_count": "Đ”ĐžĐ´Đ°Ņ‚Đž ҘĐĩ {count, plural, one {# Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēа} other {# Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēа}} ҃ аĐģĐąŅƒĐŧ", - "assets_added_to_name_count": "Đ”ĐžĐ´Đ°Ņ‚Đž {count, plural, one {# Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēа} other {# Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēĐĩ}} ҃ {hasName, select, true {{name}} other {ĐŊОви аĐģĐąŅƒĐŧ}}", "assets_count": "{count, plural, one {# Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēа} few {# Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēĐĩ} other {# Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēа}}", "assets_deleted_permanently": "{count} ĐĩĐģĐĩĐŧĐĩĐŊĐ°Ņ‚Đ° Ņ‚Ņ€Đ°Ņ˜ĐŊĐž ĐžĐąŅ€Đ¸ŅĐ°ĐŊĐž", "assets_deleted_permanently_from_server": "{count} Ņ€ĐĩŅŅƒŅ€Ņ(а) Ņ‚Ņ€Đ°Ņ˜ĐŊĐž ĐžĐąŅ€Đ¸ŅĐ°ĐŊ(а) ŅĐ° Immich ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", diff --git a/i18n/sr_Latn.json b/i18n/sr_Latn.json index aa92e6da49..a757146861 100644 --- a/i18n/sr_Latn.json +++ b/i18n/sr_Latn.json @@ -456,7 +456,6 @@ "assets": "Zapisi", "assets_added_count": "Dodato {count, plural, one {# datoteka} other {# datoteka}}", "assets_added_to_album_count": "Dodato je {count, plural, one {# datoteka} other {# datoteka}} u album", - "assets_added_to_name_count": "Dodato {count, plural, one {# datoteka} other {# datoteke}} u {hasName, select, true {{name}} other {novi album}}", "assets_count": "{count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}", "assets_deleted_permanently": "{count} elemenata trajno obrisano", "assets_deleted_permanently_from_server": "{count} resurs(a) trajno obrisan(a) sa Immich servera", diff --git a/i18n/sv.json b/i18n/sv.json index a4c08f8c22..00f5d95b66 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -22,6 +22,7 @@ "add_partner": "Lägg till partner", "add_path": "Lägg till sÃļkväg", "add_photos": "Lägg till foton", + "add_tag": "Lägg till tagg", "add_to": "Lägg till iâ€Ļ", "add_to_album": "Lägg till i album", "add_to_album_bottom_sheet_added": "Tillagd till {album}", @@ -33,6 +34,7 @@ "added_to_favorites_count": "{count, number} tillagda till favoriter", "admin": { "add_exclusion_pattern_description": "Lägg till exkluderande mÃļnster. Matchning med jokertecken *, ** samt ? stÃļdjs. FÃļr att ignorera alla filer i samtliga mappar som heter \"Raw\", använd \"**/Raw/**\". FÃļr att ignorera alla filer som slutar med \".tif\", använd \"**/*.tif\". FÃļr att ignorera en absolut sÃļkväg, använd \"/sÃļkväg/att/ignorera/**\".", + "admin_user": "Adminanvändare", "asset_offline_description": "Denna externa bibliotekstillgÃĨng finns inte längre pÃĨ disken och har flyttats till papperskorgen. Om filen flyttades inom biblioteket, kontrollera din tidslinje fÃļr den nya motsvarande tillgÃĨngen. FÃļr att ÃĨterställa denna tillgÃĨng, se till att filsÃļkvägen nedan kan nÃĨs av Immich och skanna biblioteket.", "authentication_settings": "Autentiseringsinställningar", "authentication_settings_description": "Hantera lÃļsenord, OAuth, och andra autentiseringsinställningar", @@ -169,7 +171,7 @@ "note_apply_storage_label_previous_assets": "Obs: Om du vill använda lagringsetiketten pÃĨ tidigare uppladdade tillgÃĨngar kÃļr du", "note_cannot_be_changed_later": "OBS: Detta kan inte ändras i efterhand!", "notification_email_from_address": "FrÃĨn adress", - "notification_email_from_address_description": "Avsändarens epost, t.ex.: \"Immich Fotoserver \"", + "notification_email_from_address_description": "Avsändarens e-post, till exempel: \"Immich Fotoserver \". Säkerställ att du använder en adress som du har tillÃĨtelse att skicka e-post frÃĨn.", "notification_email_host_description": "Värd fÃļr epostservern (t.ex. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorera certifikatfel", "notification_email_ignore_certificate_errors_description": "Ignorera valideringsfel fÃļr TLS-certifikat (rekommenderas ej)", @@ -194,6 +196,8 @@ "oauth_mobile_redirect_uri": "Telefonomdirigernings-URI", "oauth_mobile_redirect_uri_override": "Telefonomdirigerings-URI Ãļverrskridning", "oauth_mobile_redirect_uri_override_description": "Aktivera om OAuth-leverantÃļren inte tillÃĨter mobila URI:er, sÃĨ som ''{callback}''", + "oauth_role_claim": "RollansprÃĨk", + "oauth_role_claim_description": "Bevilja administratÃļrsÃĨtkomst automatiskt baserat pÃĨ fÃļrekomsten av detta pÃĨstÃĨende. PÃĨstÃĨendet kan innehÃĨlla antingen 'user' eller 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "Hantera OAuth-logininställningar", "oauth_settings_more_details": "FÃļr ytterligare detaljer om denna funktion, se dokumentationen.", @@ -202,7 +206,7 @@ "oauth_storage_quota_claim": "Användaranknuten lagringskvot", "oauth_storage_quota_claim_description": "Sätter automatiskt angiven användares lagringskvot.", "oauth_storage_quota_default": "Standardlagringskvot (GiB)", - "oauth_storage_quota_default_description": "Kvot i GiB som används när ingen fordran angetts (Ange 0 fÃļr obegränsad kvot).", + "oauth_storage_quota_default_description": "Kvot i GiB som används när ingen fordran angetts.", "oauth_timeout": "Begäran tog fÃļr lÃĨng tid", "oauth_timeout_description": "Timeout fÃļr fÃļrfrÃĨgningar i millisekunder", "password_enable_description": "Logga in med epost och lÃļsenord", @@ -242,6 +246,7 @@ "storage_template_migration_info": "Lagringsmallen kommer konvertera alla filändelser till gemena bokstäver. Ändringar gäller endast fÃļr nya resurser, fÃļr att retoaktivt tillämpa mallen pÃĨ befintliga resurser kÃļr {job}.", "storage_template_migration_job": "Lagringsmall migreringsjobb", "storage_template_more_details": "FÃļr mer information om den här funktionen se Lagringsmall och dess konsekvenser", + "storage_template_onboarding_description_v2": "Denna funktion kommer när den är aktiverad att auto-organisera filer baserat pÃĨ en användardefinierad mall. FÃļr mer information se dokumentationen.", "storage_template_path_length": "Uppskattad längdbegränsning pÃĨ sÃļkväg: {length, number}/{limit, number}", "storage_template_settings": "Lagringsmall", "storage_template_settings_description": "Hantera mappstruktur och filnamn fÃļr uppladdade resurser", @@ -401,6 +406,9 @@ "album_with_link_access": "LÃĨt alla med länken se foton och personer i det här albumet.", "albums": "Album", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Album}}", + "albums_default_sort_order": "Standard sorteringsordning fÃļr album", + "albums_default_sort_order_description": "Standard sorteringsordning fÃļr mediefiler vid skapande av nytt album.", + "albums_feature_description": "Samlingar av mediefiler som kan delas med andra användare.", "all": "Allt", "all_albums": "Alla album", "all_people": "Alla personer", @@ -421,6 +429,7 @@ "app_settings": "Appinställningar", "appears_in": "Visas i", "archive": "Arkiv", + "archive_action_prompt": "{count} adderade till Arkiv", "archive_or_unarchive_photo": "Arkivera eller oarkivera fotot", "archive_page_no_archived_assets": "Inga arkiverade objekt hittade", "archive_page_title": "Arkiv ({count})", @@ -458,7 +467,6 @@ "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_name_count": "Lade till {count, plural, one {# objekt} other {# objekt}} till {hasName, select, true {{name}} other {nytt album}}", "assets_count": "{count, plural, one {# objekt} other {# objekt}}", "assets_deleted_permanently": "{count} objekt har tagits bort permanent", "assets_deleted_permanently_from_server": "{count} objekt har tagits bort permanent frÃĨn Immich-servern", @@ -639,6 +647,7 @@ "confirm_password": "Bekräfta lÃļsenord", "confirm_tag_face": "Vill du tagga det här ansiktet som {name}?", "confirm_tag_face_unnamed": "Vill du tagga detta ansikte?", + "connected_device": "Ansluten enhet", "connected_to": "Ansluten till", "contain": "Anpassa", "context": "Sammanhang", @@ -691,7 +700,6 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "MÃļrk", - "darkTheme": "Växla till mÃļrkt tema", "date_after": "Datum efter", "date_and_time": "Datum och Tid", "date_before": "Datum fÃļre", @@ -707,6 +715,7 @@ "default_locale": "Standardplats", "default_locale_description": "Formatera datum och siffror baserat pÃĨ din webbläsares sprÃĨkversion", "delete": "Radera", + "delete_action_prompt": "{count, plural, one {# permanent raderad} other {# permanent raderade}}", "delete_album": "Ta bort album", "delete_api_key_prompt": "Är du säker pÃĨ att du vill ta bort denna API-nyckel?", "delete_dialog_alert": "Dessa objekt kommer att raderas permanent frÃĨn Immich och din enhet", @@ -739,6 +748,7 @@ "disallow_edits": "TillÃĨt inte redigeringar", "discord": "Discord", "discover": "Upptäck", + "discovered_devices": "Funna enheter", "dismiss_all_errors": "Avvisa alla fel", "dismiss_error": "Avvisa fel", "display_options": "Visningsalternativ", @@ -1083,6 +1093,7 @@ "invite_to_album": "Bjuder in till album", "ios_debug_info_last_sync_at": "Senaste synkning {dateTime}", "ios_debug_info_no_processes_queued": "Inga bakgrundsprocesser kÃļade", + "ios_debug_info_processes_queued": "{count, plural, one {{count} bakgrundsprocess kÃļad} other {{count} bakgrundsprocesser kÃļade}}", "items_count": "{count, plural, one {# objekt} other {# objekt}}", "jobs": "Jobb", "keep": "BehÃĨll", @@ -1091,6 +1102,8 @@ "kept_this_deleted_others": "BehÃĨll denna tillgÃĨng och borttagna {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "Kortkommandon", "language": "SprÃĨk", + "language_no_results_title": "Inga sprÃĨk funna", + "language_search_hint": "SÃļk sprÃĨkâ€Ļ", "language_setting_description": "Välj Ãļnskat sprÃĨk", "last_seen": "Senast sedd", "latest_version": "Senaste versionen", @@ -1129,6 +1142,7 @@ "locked_folder": "LÃĨst Mapp", "log_out": "Logga ut", "log_out_all_devices": "Logga ut alla enheter", + "logged_in_as": "Inloggad som {user}", "logged_out_all_devices": "Loggat ut frÃĨn alla enheter", "logged_out_device": "Loggat ut enheten", "login": "Logga in", @@ -1224,6 +1238,7 @@ "more": "Mer", "move": "Flytta", "move_off_locked_folder": "Flytta frÃĨn lÃĨst mapp", + "move_to_lock_folder_action_prompt": "{count} adderades till lÃĨst mapp", "move_to_locked_folder": "Flytta till lÃĨst mapp", "move_to_locked_folder_confirmation": "Dessa foton och videor kommer tas bort frÃĨn alla album och gÃĨr endast se i lÃĨsta mappen", "moved_to_archive": "Flyttade {count, plural, one {# resurs} other {# assets}} till arkivet", @@ -1825,6 +1840,7 @@ "unable_to_setup_pin_code": "Kunde inte konfigurera pinkod", "unarchive": "Ångra arkivering", "unarchived_count": "{count, plural, one {# borttagen frÃĨn arkiv} other {# borttagna frÃĨn arkiv}}", + "undo": "Ångra", "unfavorite": "Avfavorisera", "unhide_person": "Visa person", "unknown": "Okänd", diff --git a/i18n/ta.json b/i18n/ta.json index ce0768982d..a91ea9d045 100644 --- a/i18n/ta.json +++ b/i18n/ta.json @@ -392,7 +392,6 @@ "assets": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯", "assets_added_count": "āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯ {āŽŽāŽŖā¯āŽŖāŽŋāŽ•ā¯āŽ•ā¯ˆ, āŽĒāŽŠā¯āŽŽā¯ˆ, āŽ’āŽŠā¯āŽąā¯ {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯} āŽŽāŽąā¯āŽą {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯}}", "assets_added_to_album_count": "āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ {āŽŽāŽŖā¯āŽŖāŽŋāŽ•ā¯āŽ•ā¯ˆ, āŽĒāŽŠā¯āŽŽā¯ˆ, āŽ’āŽŠā¯āŽąā¯ {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯} āŽŽāŽąā¯āŽą {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯}}", - "assets_added_to_name_count": "āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯ {āŽŽāŽŖā¯āŽŖāŽŋāŽ•ā¯āŽ•ā¯ˆ, āŽĒāŽŠā¯āŽŽā¯ˆ, āŽ’āŽŠā¯āŽąā¯ {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯} āŽŽāŽąā¯āŽą {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯}} {hasname, āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯, āŽ‰āŽŖā¯āŽŽā¯ˆ { {name} } āŽĒāŽŋāŽą {new album}}", "assets_count": "{āŽŽāŽŖā¯āŽŖāŽŋāŽ•ā¯āŽ•ā¯ˆ, āŽĒāŽŠā¯āŽŽā¯ˆ, āŽ’āŽŠā¯āŽąā¯ {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯} āŽŽāŽąā¯āŽą {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯}}", "assets_moved_to_trash_count": "āŽ¨āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯ {āŽŽāŽŖā¯āŽŖāŽŋāŽ•ā¯āŽ•ā¯ˆ, āŽĒāŽŠā¯āŽŽā¯ˆ, āŽ’āŽŠā¯āŽąā¯ {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯} āŽŽāŽąā¯āŽą {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯}}}", "assets_permanently_deleted_count": "āŽ¨āŽŋāŽ°āŽ¨ā¯āŽ¤āŽ°āŽŽāŽžāŽ• āŽ¨ā¯€āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯ {āŽŽāŽŖā¯āŽŖāŽŋāŽ•ā¯āŽ•ā¯ˆ, āŽĒāŽŠā¯āŽŽā¯ˆ, āŽ’āŽŠā¯āŽąā¯ {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯} āŽĒāŽŋāŽą {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯}}", diff --git a/i18n/te.json b/i18n/te.json index 8bd57bc3bc..4a2e938c9a 100644 --- a/i18n/te.json +++ b/i18n/te.json @@ -23,6 +23,8 @@ "add_photos": "ā°Ģāą‹ā°Ÿāą‹ā°˛ā°¨āą ā°œāą‹ā°Ąā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", "add_to": "ā°œāą‹ā°Ąā°ŋā°‚ā°šā°‚ā°Ąā°ŋâ€Ļ", "add_to_album": "ā°†ā°˛āąā°Ŧā°Žāąâ€Œā°•āą ā°œāą‹ā°Ąā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", + "add_to_album_bottom_sheet_added": "ā°†ā°˛āąā°Ŧā°Žāąā°•āą ā°œāą‹ā°Ąā°ŋā°‚ā°šā°Ŧā°Ąā°ŋā°‚ā°Ļā°ŋ", + "add_to_album_bottom_sheet_already_exists": "ā°†ā°˛āąā°Ŧā°Žāąâ€Œā°˛āą‹ ā°‡ā°Ēāąā°ĒⰟā°ŋā°•āą‡ ā°œāą‹ā°Ąā°ŋā°‚ā°šā°Ŧā°Ąā°ŋā°‚ā°Ļā°ŋ", "add_to_shared_album": "ā°­ā°žā°—ā°¸āąā°ĩā°žā°Žāąā°¯ ā°†ā°˛āąā°Ŧā°Žāąâ€Œā°•āą ā°œāą‹ā°Ąā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", "add_url": "URLā°¨ā°ŋ ā°œāą‹ā°Ąā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", "added_to_archive": "ā°†ā°°āąā°•āąˆā°ĩāąâ€Œā°•ā°ŋ ā°œāą‹ā°Ąā°ŋā°‚ā°šā°Ŧā°Ąā°ŋā°‚ā°Ļā°ŋ", @@ -30,17 +32,18 @@ "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": "ā°Žā°ŗāąā°˛āą€ ā°Ēāąā°°ā°žā°°ā°‚ā°Ŧā°ŋā°‚ā°šā°Ÿā°žā°¨ā°ŋā°•ā°ŋ, Server Commandā°¨ā°ŋ ā°‰ā°Ēā°¯āą‹ā°—ā°ŋā°‚ā°šā°‚ā°Ąā°ŋ.", "background_task_job": "ā°¨āą‡ā°Ēā°Ĩāąā°¯ ā°Ēā°¨āąā°˛āą", - "backup_database": "ā°Ŧāąā°¯ā°žā°•ā°Ēāą ā°Ąāą‡ā°Ÿā°žā°Ŧāą‡ā°¸āą", - "backup_database_enable_description": "ā°Ąāą‡ā°Ÿā°žā°Ŧāą‡ā°¸āą ā°Ŧāąā°¯ā°žā°•ā°Ēāąâ€Œā°˛ā°¨āą ā°Ēāąā°°ā°žā°°ā°‚ā°­ā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", - "backup_keep_last_amount": "ā°‰ā°‚ā°šāąā°•āą‹ā°ĩā°žā°˛āąā°¸ā°ŋā°¨ ā°Žāąā°¨āąā°ĒⰟā°ŋ ā°Ŧāąā°¯ā°žā°•ā°Ēāąâ€Œā°˛ ā°ŽāąŠā°¤āąā°¤ā°‚", - "backup_settings": "ā°Ŧāąā°¯ā°žā°•ā°Ēāą ā°¸āą†ā°Ÿāąā°Ÿā°ŋā°‚ā°—āąâ€Œā°˛āą", - "backup_settings_description": "ā°Ąāą‡ā°Ÿā°žā°Ŧāą‡ā°¸āą ā°Ŧāąā°¯ā°žā°•ā°Ēāą ā°¸āą†ā°Ÿāąā°Ÿā°ŋā°‚ā°—āąâ€Œā°˛ā°¨āą ā°¨ā°ŋā°°āąā°ĩā°šā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", + "backup_database": "ā°Ąāą‡ā°Ÿā°žā°Ŧāą‡ā°¸āą ā°¨āą ā°¸āąƒā°ˇāąā°Ÿā°ŋā°‚ā°šāą", + "backup_database_enable_description": "ā°Ąāą‡ā°Ÿā°žā°Ŧāą‡ā°¸āą ā°Ēā°Ąā°ĩāą†ā°¯āąā°¯ā°Ąā°žā°¨āąā°¨āą€ ā°Ēāąā°°ā°žā°°ā°‚ā°­ā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", + "backup_keep_last_amount": "ā°‰ā°‚ā°šāąā°•āą‹ā°ĩā°žā°˛āąā°¸ā°ŋā°¨ ā°Žāąā°¨āąā°ĒⰟā°ŋ ā°Ēā°Ąā°ĩāą†ā°¯āąā°¯ā°Ąā°žā°˛āąā°˛ā°ž ā°ŽāąŠā°¤āąā°¤ā°‚", + "backup_settings": "ā°Ąāą‡ā°Ÿā°žā°Ŧāą‡ā°¸āą ā°Ēā°Ąā°ĩāą†ā°¸āą‡ ā°¸āą†ā°Ÿāąā°Ÿā°ŋā°‚ā°—āąâ€Œā°˛āą", + "backup_settings_description": "ā°Ąāą‡ā°Ÿā°žā°Ŧāą‡ā°¸āą ā°Ēā°Ąā°ĩāą†ā°¸āą‡ ā°¸āą†ā°Ÿāąā°Ÿā°ŋā°‚ā°—āąâ€Œā°˛ā°¨āą ā°¨ā°ŋā°°āąā°ĩā°šā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", "cleared_jobs": "ā°Ļāą€ā°¨ā°ŋ ā°•āą‹ā°¸ā°‚ ā°‰ā°Ļāąā°¯āą‹ā°—ā°žā°˛āą ā°•āąā°˛ā°ŋā°¯ā°°āą ā°šāą‡ā°¯ā°Ŧā°Ąāąā°Ąā°žā°¯ā°ŋ: {job}", "config_set_by_file": "ā°•ā°žā°¨āąā°Ģā°ŋā°—ā°°āą‡ā°ˇā°¨āą ā°Ēāąā°°ā°¸āąā°¤āąā°¤ā°‚ ā°•ā°žā°¨āąā°Ģā°ŋā°—ā°°āą‡ā°ˇā°¨āą ā°Ģāąˆā°˛āą ā°Ļāąā°ĩā°žā°°ā°ž ā°¸āą†ā°Ÿāą ā°šāą‡ā°¯ā°Ŧā°Ąā°ŋā°‚ā°Ļā°ŋ", "confirm_delete_library": "ā°Žāą€ā°°āą ā°–ā°šāąā°šā°ŋā°¤ā°‚ā°—ā°ž {library} ā°˛āąˆā°Ŧāąā°°ā°°āą€ā°¨ā°ŋ ā°¤āąŠā°˛ā°—ā°ŋā°‚ā°šā°žā°˛ā°¨āąā°•āąā°‚ā°Ÿāąā°¨āąā°¨ā°žā°°ā°ž?", @@ -48,6 +51,7 @@ "confirm_email_below": "ā°¨ā°ŋā°°āąā°§ā°žā°°ā°ŋā°‚ā°šā°Ąā°žā°¨ā°ŋā°•ā°ŋ, ā°•āąā°°ā°ŋā°‚ā°Ļ \"{email}\" ā°Ÿāąˆā°Ēāą ā°šāą‡ā°¯ā°‚ā°Ąā°ŋ", "confirm_reprocess_all_faces": "ā°Žāą€ā°°āą ā°–ā°šāąā°šā°ŋā°¤ā°‚ā°—ā°ž ā°…ā°¨āąā°¨ā°ŋ ā°Žāąā°–ā°žā°˛ā°¨āą ā°°āą€ā°Ēāąā°°ā°žā°¸āą†ā°¸āą ā°šāą‡ā°¯ā°žā°˛ā°¨āąā°•āąā°‚ā°Ÿāąā°¨āąā°¨ā°žā°°ā°ž? ā°‡ā°Ļā°ŋ ā°Ēāą‡ā°°āąā°¨āąā°¨ ā°ĩāąā°¯ā°•āąā°¤āąā°˛ā°¨āą ā°•āą‚ā°Ąā°ž ā°•āąā°˛ā°ŋā°¯ā°°āą ā°šāą‡ā°¸āąā°¤āąā°‚ā°Ļā°ŋ.", "confirm_user_password_reset": "ā°Žāą€ā°°āą ā°–ā°šāąā°šā°ŋā°¤ā°‚ā°—ā°ž {user} ā°Ēā°žā°¸āąâ€Œā°ĩā°°āąā°Ąāąâ€Œā°¨ā°ŋ ā°°āą€ā°¸āą†ā°Ÿāą ā°šāą‡ā°¯ā°žā°˛ā°¨āąā°•āąā°‚ā°Ÿāąā°¨āąā°¨ā°žā°°ā°ž?", + "confirm_user_pin_code_reset": "ā°Žāą€ā°°āą ā°–ā°šāąā°šā°ŋā°¤ā°‚ā°—ā°ž {user} ā°¯āąŠā°•āąā°• ā°Ēā°ŋā°¨āą ā°•āą‹ā°Ąāą ā°¨āą€ ā°°āą€ā°¸āą†ā°Ÿāą ā°šāą‡ā°Ļāąā°Ļā°žā°Žā°¨ā°ŋā°…ā°¨āąā°•āąā°‚ā°Ÿāąā°¨āąā°¨ā°žā°°ā°ž?", "create_job": "ā°Ēā°¨ā°ŋā°¨ā°ŋ ā°¸āąƒā°ˇāąā°Ÿā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", "cron_expression": "ā°•āąā°°ā°žā°¨āą ā°ĩāąā°¯ā°•āąā°¤āą€ā°•ā°°ā°Ŗ", "cron_expression_description": "ā°•āąā°°ā°žā°¨āą ā°Ģā°žā°°āąā°Žā°žā°Ÿāą ā°‰ā°Ēā°¯āą‹ā°—ā°ŋā°‚ā°šā°ŋ ā°¸āąā°•ā°žā°¨ā°ŋā°‚ā°—āą ā°ĩā°ŋā°°ā°žā°Žā°žā°¨āąā°¨ā°ŋ ā°¸āą†ā°Ÿāą ā°šāą‡ā°¯ā°‚ā°Ąā°ŋ. ā°Žā°°ā°ŋā°¨āąā°¨ā°ŋ ā°ĩā°ŋā°ĩā°°ā°žā°˛ ā°•āą‹ā°¸ā°‚ ā°Ļā°¯ā°šāą‡ā°¸ā°ŋ ā°‰ā°Ļā°ž. ā°•āąā°°āą‹ā°‚ā°Ÿā°žā°Ŧāą ā°—āąā°°āą ā°šāą‚ā°Ąā°‚ā°Ąā°ŋ", @@ -402,7 +406,6 @@ "assets": "ā°†ā°¸āąā°¤āąā°˛āą", "assets_added_count": "ā°œāą‹ā°Ąā°ŋā°‚ā°šā°Ŧā°Ąā°ŋā°¨ā°ĩā°ŋ {count, plural, one {# ā°†ā°¸āąā°¤ā°ŋ} other {# ā°†ā°¸āąā°¤āąā°˛āą}}", "assets_added_to_album_count": "{count, plural, one {# ā°†ā°¸āąā°¤ā°ŋ} other {# ā°†ā°¸āąā°¤āąā°˛āą}} ā°†ā°˛āąā°Ŧā°Žāąâ€Œā°•ā°ŋ ā°œāą‹ā°Ąā°ŋā°‚ā°šā°Ŧā°Ąā°ŋā°¨ā°ĩā°ŋ", - "assets_added_to_name_count": "{count, plural, one {# ā°†ā°¸āąā°¤ā°ŋ} other {# ā°†ā°¸āąā°¤āąā°˛āą}} {hasName, select, true {{name}} other {ā°•āąŠā°¤āąā°¤ ā°†ā°˛āąā°Ŧā°Žāą}}ā°•ā°ŋ ā°œāą‹ā°Ąā°ŋā°‚ā°šā°Ŧā°Ąā°ŋā°¨ā°ĩā°ŋ", "assets_count": "{count, plural, one {# ā°†ā°¸āąā°¤ā°ŋ} other {# ā°†ā°¸āąā°¤āąā°˛āą}}", "assets_moved_to_trash_count": "{count, plural, one {# ā°†ā°¸āąā°¤ā°ŋ} other {# ā°†ā°¸āąā°¤āąā°˛āą}} ā°šāą†ā°¤āąā°¤ā°Ŧāąā°Ÿāąā°Ÿā°˛āą‹ā°•ā°ŋ ⰤⰰⰞā°ŋā°‚ā°šā°žā°°āą", "assets_permanently_deleted_count": "{count, plural, one {# ā°†ā°¸āąā°¤ā°ŋ} other {# ā°†ā°¸āąā°¤āąā°˛āą}} ā°ļā°žā°ļāąā°ĩā°¤ā°‚ā°—ā°ž ā°¤āąŠā°˛ā°—ā°ŋā°‚ā°šā°Ŧā°Ąā°ŋā°¨ā°ĩā°ŋ", diff --git a/i18n/th.json b/i18n/th.json index 13d9514d63..3cce9afd1c 100644 --- a/i18n/th.json +++ b/i18n/th.json @@ -14,14 +14,15 @@ "add_a_location": "āš€ā¸žā¸´āšˆā¸Ąā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡", "add_a_name": "āš€ā¸žā¸´āšˆā¸Ąā¸Šā¸ˇāšˆā¸­", "add_a_title": "āš€ā¸žā¸´āšˆā¸Ąā¸Ģā¸ąā¸§ā¸‚āš‰ā¸­", - "add_endpoint": "āš€ā¸žā¸´āšˆā¸Ąā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸›ā¸Ĩ⏞ā¸ĸ⏗⏞⏇", + "add_endpoint": "āš€ā¸žā¸´āšˆā¸Ąā¸›ā¸Ĩ⏞ā¸ĸ⏗⏞⏇", "add_exclusion_pattern": "āš€ā¸žā¸´āšˆā¸Ąā¸‚āš‰ā¸­ā¸ĸā¸āš€ā¸§āš‰ā¸™", "add_import_path": "āš€ā¸žā¸´āšˆā¸Ąāš€ā¸Ēāš‰ā¸™ā¸—ā¸˛ā¸‡ā¸™ā¸ŗāš€ā¸‚āš‰ā¸˛", "add_location": "āš€ā¸žā¸´āšˆā¸Ąā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡", "add_more_users": "āš€ā¸žā¸´āšˆā¸Ąā¸œā¸šāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", - "add_partner": "āš€ā¸žā¸´āšˆā¸Ąā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗ", + "add_partner": "āš€ā¸žā¸´āšˆā¸Ąā¸„ā¸šāšˆā¸Ģā¸š", "add_path": "āš€ā¸žā¸´āšˆā¸Ąā¸žā¸˛ā¸—ā¸—ā¸ĩāšˆā¸•ā¸ąāš‰ā¸‡", "add_photos": "āš€ā¸žā¸´āšˆā¸Ąā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", + "add_tag": "āš€ā¸žā¸´āšˆā¸Ąāšā¸—āš‡ā¸", "add_to": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ â€Ļ", "add_to_album": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "add_to_album_bottom_sheet_added": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ {album}", @@ -33,7 +34,8 @@ "added_to_favorites_count": "{count, number} ā¸Ŗā¸šā¸›ā¸–ā¸šā¸āš€ā¸žā¸´āšˆā¸Ąāš€ā¸‚āš‰ā¸˛ā¸Ŗā¸˛ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”", "admin": { "add_exclusion_pattern_description": "āš€ā¸žā¸´āšˆā¸Ąā¸Ŗā¸šā¸›āšā¸šā¸šā¸‚āš‰ā¸­ā¸ĸā¸āš€ā¸§āš‰ā¸™ ā¸Ŗā¸­ā¸‡ā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗāšƒā¸Šāš‰ *, ** āšā¸Ĩ⏰ ? ā¸Ģā¸˛ā¸ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩā¸°āš€ā¸§āš‰ā¸™āš„ā¸Ÿā¸ĨāšŒā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āšƒā¸™āš„ā¸”āš€ā¸Ŗāš‡ā¸ā¸—ā¸­ā¸Ŗā¸ĩ⏗ā¸ĩāšˆā¸Šā¸ˇāšˆā¸­ā¸§āšˆā¸˛ \"Raw\" āšƒā¸Ģāš‰āšƒā¸Šāš‰ \"**/Raw/**\" ā¸–āš‰ā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩā¸°āš€ā¸§āš‰ā¸™āš„ā¸Ÿā¸ĨāšŒā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸—ā¸ĩāšˆā¸Ĩā¸‡ā¸—āš‰ā¸˛ā¸ĸā¸”āš‰ā¸§ā¸ĸ \".tif\" āšƒā¸Ģāš‰āšƒā¸Šāš‰ \"**/*.tif\" ā¸–āš‰ā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩā¸°āš€ā¸§āš‰ā¸™ā¸žā¸˛ā¸˜ā¸—ā¸ĩāšˆāš€ā¸Ŗā¸´āšˆā¸Ąā¸ˆā¸˛ā¸āš„ā¸”āš€ā¸Ŗā¸ā¸—ā¸­ā¸Ŗā¸ĩā¸šā¸™ā¸Ēā¸¸ā¸”āšƒā¸Ģāš‰āšƒā¸Šāš‰ \"/ā¸žā¸˛ā¸˜/⏗ā¸ĩāšˆā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗ/ā¸Ĩā¸°āš€ā¸§āš‰ā¸™/**\"", - "asset_offline_description": "āš„ā¸Ÿā¸ĨāšŒ Asset ā¸‚ā¸­ā¸‡āš„ā¸Ĩ⏚⏪⏞⏪ā¸ĩ⏠⏞ā¸ĸ⏙⏭⏁⏙ā¸ĩāš‰āš„ā¸Ąāšˆā¸žā¸šāšƒā¸™ā¸”ā¸´ā¸Ēā¸āšŒāšā¸Ĩāš‰ā¸§ āšā¸Ĩā¸°ā¸–ā¸šā¸ā¸ĸāš‰ā¸˛ā¸ĸāš„ā¸›ā¸—ā¸ĩāšˆā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰ ā¸Ģā¸˛ā¸āš„ā¸Ÿā¸ĨāšŒā¸–ā¸šā¸ā¸ĸāš‰ā¸˛ā¸ĸ⏠⏞ā¸ĸāšƒā¸™āš„ā¸Ĩ⏚⏪⏞⏪ā¸ĩ āš‚ā¸›ā¸Ŗā¸”ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šāš„ā¸—ā¸ĄāšŒāš„ā¸Ĩā¸™āšŒā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš€ā¸žā¸ˇāšˆā¸­ā¸Ģā¸˛āšā¸­ā¸Ēāš€ā¸‹āš‡ā¸•ā¸—ā¸ĩāšˆāš€ā¸ā¸ĩāšˆā¸ĸā¸§ā¸‚āš‰ā¸­ā¸‡āšƒā¸Ģā¸Ąāšˆ ā¸Ģā¸˛ā¸ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸ā¸šāš‰ā¸„ā¸ˇā¸™ Asset ⏙ā¸ĩāš‰ āš‚ā¸›ā¸Ŗā¸”ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šāšƒā¸Ģāš‰āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ Immich ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡āš€ā¸Ēāš‰ā¸™ā¸—ā¸˛ā¸‡āš„ā¸Ÿā¸ĨāšŒā¸”āš‰ā¸˛ā¸™ā¸Ĩāšˆā¸˛ā¸‡āš„ā¸”āš‰ āšā¸Ĩ⏰⏗⏺⏁⏞⏪ā¸Ēāšā¸ā¸™āš„ā¸Ĩ⏚⏪⏞⏪ā¸ĩ⏭ā¸ĩā¸ā¸„ā¸Ŗā¸ąāš‰ā¸‡", + "admin_user": "ā¸œā¸šāš‰ā¸”ā¸šāšā¸Ĩ", + "asset_offline_description": "āš„ā¸Ąāšˆā¸žā¸šāš„ā¸Ÿā¸ĨāšŒā¸Ēā¸ˇāšˆā¸­ā¸‚ā¸­ā¸‡āš„ā¸Ĩ⏚⏪⏞⏪ā¸ĩ⏠⏞ā¸ĸ⏙⏭⏁⏙ā¸ĩāš‰āšƒā¸™ā¸”ā¸´ā¸Ēā¸āšŒ āšā¸Ĩā¸°ā¸–ā¸šā¸ā¸ĸāš‰ā¸˛ā¸ĸāš„ā¸›ā¸—ā¸ĩāšˆā¸–ā¸ąā¸‡ā¸‚ā¸ĸā¸°āšā¸Ĩāš‰ā¸§ ā¸Ģā¸˛ā¸āš„ā¸Ÿā¸ĨāšŒā¸–ā¸šā¸ā¸ĸāš‰ā¸˛ā¸ĸ⏠⏞ā¸ĸāšƒā¸™āš„ā¸Ĩ⏚⏪⏞⏪ā¸ĩ āš‚ā¸›ā¸Ŗā¸”ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šāš„ā¸—ā¸ĄāšŒāš„ā¸Ĩā¸™āšŒā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš€ā¸žā¸ˇāšˆā¸­ā¸Ģ⏞ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆāš€ā¸ā¸ĩāšˆā¸ĸā¸§ā¸‚āš‰ā¸­ā¸‡āšƒā¸Ģā¸Ąāšˆ ā¸Ģā¸˛ā¸ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸ā¸šāš‰ā¸„ā¸ˇā¸™ā¸Ēā¸ˇāšˆā¸­ā¸™ā¸ĩāš‰ āš‚ā¸›ā¸Ŗā¸”ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šā¸§āšˆā¸˛ Immich ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡āš„ā¸Ÿā¸ĨāšŒā¸”āš‰ā¸˛ā¸™ā¸Ĩāšˆā¸˛ā¸‡āš„ā¸”āš‰ āšā¸Ĩ⏰⏗⏺⏁⏞⏪ā¸Ēāšā¸ā¸™āš„ā¸Ĩ⏚⏪⏞⏪ā¸ĩ⏭ā¸ĩā¸ā¸„ā¸Ŗā¸ąāš‰ā¸‡", "authentication_settings": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗāš€ā¸‚āš‰ā¸˛ā¸–ā¸ļ⏇", "authentication_settings_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸Ŗā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™, OAuth, āšā¸Ĩā¸°ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗāš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡ā¸­ā¸ˇāšˆā¸™āš†", "authentication_settings_disable_all": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸›ā¸´ā¸”ā¸§ā¸´ā¸˜ā¸ĩ⏁⏞⏪ā¸Ĩāš‡ā¸­ā¸ā¸­ā¸´ā¸™ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ? ā¸Ĩāš‡ā¸­ā¸ā¸­ā¸´ā¸™ā¸ˆā¸°ā¸–ā¸šā¸ā¸›ā¸´ā¸”ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", @@ -43,7 +45,7 @@ "backup_database_enable_description": "āš€ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸ā¸˛ā¸™ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "backup_keep_last_amount": "ā¸ˆā¸ŗā¸™ā¸§ā¸™ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸āšˆā¸­ā¸™ā¸Ģā¸™āš‰ā¸˛ā¸—ā¸ĩāšˆā¸•āš‰ā¸­ā¸‡āš€ā¸āš‡ā¸šāš„ā¸§āš‰", "backup_settings": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", - "backup_settings_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸ā¸˛ā¸™ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ ⏇⏞⏙ā¸Ē⏺⏪⏭⏇⏙ā¸ĩāš‰ā¸ˆā¸°āš„ā¸Ąāšˆā¸–ā¸šā¸ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šāšā¸Ĩā¸°ā¸„ā¸¸ā¸“ā¸ˆā¸°āš„ā¸Ąāšˆāš„ā¸”āš‰ā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸Ąā¸ˇāšˆā¸­ā¸Ąā¸ąā¸™ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡āš„ā¸Ąāšˆā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", + "backup_settings_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸ā¸˛ā¸™ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "cleared_jobs": "āš€ā¸„ā¸Ĩā¸ĩā¸ĸā¸ŖāšŒā¸‡ā¸˛ā¸™ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸š: {job}", "config_set_by_file": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸„ā¸­ā¸™ā¸Ÿā¸´ā¸ā¸ā¸ŗā¸Ĩā¸ąā¸‡ā¸–ā¸šā¸ā¸ā¸ŗā¸Ģā¸™ā¸”āš‚ā¸”ā¸ĸāš„ā¸Ÿā¸ĨāšŒā¸„ā¸­ā¸™ā¸Ÿā¸´ā¸", "confirm_delete_library": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸­ā¸ĸ⏞⏁ā¸Ĩā¸šā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸ž {library} ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", @@ -169,7 +171,7 @@ "note_apply_storage_label_previous_assets": "ā¸Ģā¸˛ā¸ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗāšƒā¸Šāš‰ Storage Label ā¸ā¸ąā¸šāš„ā¸Ÿā¸ĨāšŒā¸—ā¸ĩāšˆā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”ā¸āšˆā¸­ā¸™ā¸Ģā¸™āš‰ā¸˛ā¸™ā¸ĩāš‰ āšƒā¸Ģāš‰ā¸Ŗā¸ąā¸™ā¸„ā¸ŗā¸Ēā¸ąāšˆā¸‡ā¸™ā¸ĩāš‰", "note_cannot_be_changed_later": "ā¸Ģā¸Ąā¸˛ā¸ĸāš€ā¸Ģ⏕⏏: āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏠⏞ā¸ĸā¸Ģā¸Ĩā¸ąā¸‡āš„ā¸”āš‰!", "notification_email_from_address": "ā¸ˆā¸˛ā¸ā¸—ā¸ĩāšˆā¸­ā¸ĸā¸šāšˆ", - "notification_email_from_address_description": "⏭ā¸ĩāš€ā¸Ąā¸Ĩā¸œā¸šāš‰ā¸Ēāšˆā¸‡ ⏭ā¸ĸāšˆā¸˛ā¸‡āš€ā¸Šāšˆā¸™ \"Immich Photo Server \"", + "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 ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔ (āš„ā¸Ąāšˆāšā¸™ā¸°ā¸™ā¸ŗ)", @@ -193,7 +195,7 @@ "oauth_enable_description": "ā¸Ĩāš‡ā¸­ā¸ā¸­ā¸´ā¸™ā¸œāšˆā¸˛ā¸™ OAuth", "oauth_mobile_redirect_uri": "URI āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āš€ā¸Ēāš‰ā¸™ā¸—ā¸˛ā¸‡ā¸šā¸™āš‚ā¸—ā¸Ŗā¸¨ā¸ąā¸žā¸—āšŒ", "oauth_mobile_redirect_uri_override": "āšā¸—ā¸™ā¸—ā¸ĩāšˆ URI āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āš€ā¸Ēāš‰ā¸™ā¸—ā¸˛ā¸‡ā¸šā¸™āš‚ā¸—ā¸Ŗā¸¨ā¸ąā¸žā¸—āšŒ", - "oauth_mobile_redirect_uri_override_description": "āš€ā¸›ā¸´ā¸”āš€ā¸Ąā¸ˇāšˆā¸­ 'app.immich:/' āš€ā¸›āš‡ā¸™ URI ⏗ā¸ĩāšˆāš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āš€ā¸Ēāš‰ā¸™ā¸—ā¸˛ā¸‡āš„ā¸Ąāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡", + "oauth_mobile_redirect_uri_override_description": "āš€ā¸›ā¸´ā¸”āš€ā¸Ąā¸ˇāšˆā¸­ā¸œā¸šāš‰āšƒā¸Ģāš‰ā¸šā¸Ŗā¸´ā¸ā¸˛ā¸Ŗ OAuth āš„ā¸Ąāšˆā¸­ā¸™ā¸¸ā¸ā¸˛ā¸• URI āš€ā¸Šāšˆā¸™ \"{callback}\"", "oauth_settings": "OAuth", "oauth_settings_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸Ĩāš‡ā¸­ā¸ā¸­ā¸´ā¸™ā¸œāšˆā¸˛ā¸™ OAuth", "oauth_settings_more_details": "ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸Ŗā¸˛ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸā¸”āš€ā¸žā¸´āšˆā¸Ąāš€ā¸•ā¸´ā¸Ą āšƒā¸Ģāš‰ā¸­āš‰ā¸˛ā¸‡ā¸–ā¸ļā¸‡āš€ā¸­ā¸ā¸Ē⏞⏪", @@ -202,7 +204,7 @@ "oauth_storage_quota_claim": "ā¸Ēā¸´ā¸—ā¸˜ā¸´āšŒā¸—ā¸ĩāšˆāšƒā¸Šāš‰ā¸­āš‰ā¸˛ā¸‡ā¸–ā¸ļā¸‡āš‚ā¸„ā¸§ā¸•āš‰ā¸˛ā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸ˆā¸ąā¸”āš€ā¸āš‡ā¸š", "oauth_storage_quota_claim_description": "ā¸•ā¸ąāš‰ā¸‡āš‚ā¸„ā¸§ā¸•āš‰ā¸˛ā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸ˆā¸ąā¸”āš€ā¸āš‡ā¸šā¸‚ā¸­ā¸‡ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ā¸•ā¸˛ā¸Ąā¸Ēā¸´ā¸—ā¸˜ā¸´āšŒā¸—ā¸ĩāšˆāšƒā¸Šāš‰ā¸­āš‰ā¸˛ā¸‡ā¸–ā¸ļā¸‡āš‚ā¸”ā¸ĸā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´", "oauth_storage_quota_default": "āš‚ā¸„ā¸§ā¸•āš‰ā¸˛ā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆāš€ā¸āš‡ā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™ (GiB)", - "oauth_storage_quota_default_description": "āš‚ā¸„ā¸§ā¸•āš‰ā¸˛āšƒā¸™ā¸Ģā¸™āšˆā¸§ā¸ĸ GiB ⏗ā¸ĩāšˆā¸ˆā¸°āšƒā¸Šāš‰āš€ā¸Ąā¸ˇāšˆā¸­āš„ā¸Ąāšˆā¸Ąā¸ĩā¸ā¸˛ā¸Ŗā¸­āš‰ā¸˛ā¸‡ā¸Ēā¸´ā¸—ā¸˜ā¸´āšŒ (ā¸›āš‰ā¸­ā¸™ 0 ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šāš‚ā¸„ā¸§ā¸•āš‰ā¸˛āš„ā¸Ąāšˆā¸ˆā¸ŗā¸ā¸ąā¸”)", + "oauth_storage_quota_default_description": "āš‚ā¸„ā¸§ā¸•āš‰ā¸˛āšƒā¸™ā¸Ģā¸™āšˆā¸§ā¸ĸ GiB ⏗ā¸ĩāšˆā¸ˆā¸°āšƒā¸Šāš‰āš€ā¸Ąā¸ˇāšˆā¸­āš„ā¸Ąāšˆā¸Ąā¸ĩā¸ā¸˛ā¸Ŗā¸­āš‰ā¸˛ā¸‡ā¸Ēā¸´ā¸—ā¸˜ā¸´āšŒ", "oauth_timeout": "ā¸Ģā¸Ąā¸”āš€ā¸§ā¸Ĩā¸˛ā¸ā¸˛ā¸Ŗā¸Ŗāš‰ā¸­ā¸‡ā¸‚ā¸­", "oauth_timeout_description": "⏪⏰ā¸ĸā¸°āš€ā¸§ā¸Ĩ⏞ā¸Ģā¸Ąā¸”āš€ā¸§ā¸Ĩ⏞ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗā¸Ŗāš‰ā¸­ā¸‡ā¸‚ā¸­ (ā¸Ģā¸™āšˆā¸§ā¸ĸāš€ā¸›āš‡ā¸™ā¸Ąā¸´ā¸Ĩā¸Ĩ⏴⏧⏴⏙⏞⏗ā¸ĩ)", "password_enable_description": "ā¸Ĩāš‡ā¸­ā¸ā¸­ā¸´ā¸™ā¸ā¸ąā¸šā¸­ā¸ĩāš€ā¸Ąā¸Ĩāšā¸Ĩ⏰⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™", @@ -242,6 +244,7 @@ "storage_template_migration_info": "āš€ā¸—ā¸Ąāš€ā¸žā¸Ĩā¸•ā¸‚ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸ˆā¸ąā¸”āš€ā¸āš‡ā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸ˆā¸°āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸•ā¸ąā¸§ā¸­ā¸ąā¸ā¸Šā¸Ŗāš€ā¸›āš‡ā¸™ā¸•ā¸ąā¸§ā¸žā¸´ā¸Ąā¸žāšŒāš€ā¸Ĩāš‡ā¸ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸” ā¸ā¸˛ā¸Ŗāš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸›ā¸Ĩā¸‡āš€ā¸—ā¸Ąāš€ā¸žā¸Ĩā¸•ā¸ˆā¸°ā¸Ąā¸ĩ⏜ā¸Ĩā¸ā¸ąā¸šāšā¸­ā¸Ēāš€ā¸‹āš‡ā¸•āšƒā¸Ģā¸Ąāšˆāš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™ ā¸Ģā¸˛ā¸ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸™ā¸ŗāš€ā¸—ā¸Ąāš€ā¸žā¸Ĩā¸•āš„ā¸›āšƒā¸Šāš‰ā¸ā¸ąā¸š Asset ⏗ā¸ĩāšˆā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”ā¸āšˆā¸­ā¸™ā¸Ģā¸™āš‰ā¸˛ā¸™ā¸ĩāš‰ āšƒā¸Ģāš‰ā¸Ŗā¸ąā¸™ {job}.", "storage_template_migration_job": "āš€ā¸—ā¸Ąāš€ā¸žā¸Ĩ⏕⏁⏞⏪ Migration ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "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": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗāš‚ā¸„ā¸Ŗā¸‡ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒāšā¸Ĩā¸°ā¸Šā¸ˇāšˆā¸­āš„ā¸Ÿā¸ĨāšŒā¸—ā¸ĩāšˆā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔", @@ -256,7 +259,7 @@ "template_email_update_album": "ā¸­ā¸ąā¸›āš€ā¸”ā¸•āš€ā¸—ā¸Ąāš€ā¸žā¸Ĩā¸•ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "template_email_welcome": "āš€ā¸—ā¸Ąāš€ā¸žā¸Ĩ⏕ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸­ā¸ĩāš€ā¸Ąā¸Ĩā¸•āš‰ā¸­ā¸™ā¸Ŗā¸ąā¸š", "template_settings": "āš€ā¸—ā¸Ąāš€ā¸žā¸Ĩā¸•ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™", - "template_settings_description": "ā¸›ā¸Ŗā¸ąā¸šāšā¸•āšˆā¸‡āš€ā¸—ā¸Ąāš€ā¸žā¸Ĩā¸•āšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™", + "template_settings_description": "ā¸›ā¸Ŗā¸ąā¸šāšā¸•āšˆā¸‡āš€ā¸—ā¸Ąāš€ā¸žā¸Ĩā¸•ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™", "theme_custom_css_settings": "CSS ā¸āšā¸˛ā¸Ģā¸™ā¸”āš€ā¸­ā¸‡", "theme_custom_css_settings_description": "Cascading Style Sheets ā¸Šāšˆā¸§ā¸ĸāšƒā¸Ģāš‰ā¸›ā¸Ŗā¸ąā¸šāšā¸•āšˆā¸‡āš€ā¸„āš‰ā¸˛āš‚ā¸„ā¸Ŗā¸‡ Immich āš„ā¸”āš‰", "theme_settings": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸˜ā¸ĩā¸Ą", @@ -362,7 +365,7 @@ "advanced_settings_proxy_headers_subtitle": "⏁⏺ā¸Ģ⏙⏔ proxy headers ⏗ā¸ĩāšˆ Immich ⏄⏧⏪ā¸Ēāšˆā¸‡ā¸žā¸Ŗāš‰ā¸­ā¸Ąā¸ā¸ąā¸šāšā¸•āšˆā¸Ĩā¸°ā¸„ā¸ŗā¸‚ā¸­āš€ā¸„ā¸Ŗā¸ˇā¸­ā¸‚āšˆā¸˛ā¸ĸ", "advanced_settings_proxy_headers_title": "ā¸žāš‡ā¸­ā¸ā¸‹ā¸ĩāšˆ āš€ā¸Žā¸”āš€ā¸”ā¸­ā¸ŖāšŒ", "advanced_settings_self_signed_ssl_subtitle": "ā¸‚āš‰ā¸˛ā¸Ąā¸ā¸˛ā¸Ŗā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šāšƒā¸šā¸Ŗā¸ąā¸šā¸Ŗā¸­ā¸‡ SSL ā¸ˆā¸ŗāš€ā¸›āš‡ā¸™ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šāšƒā¸šā¸Ŗā¸ąā¸šā¸Ŗā¸­ā¸‡āšā¸šā¸š self-signed", - "advanced_settings_self_signed_ssl_title": "ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āšƒā¸šā¸Ŗā¸ąā¸šā¸Ŗā¸­ā¸‡ SSL āšā¸šā¸š self-signed ", + "advanced_settings_self_signed_ssl_title": "ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āšƒā¸šā¸Ŗā¸ąā¸šā¸Ŗā¸­ā¸‡ SSL āšā¸šā¸š self-signed", "advanced_settings_sync_remote_deletions_subtitle": "⏚ā¸Ģā¸Ŗā¸ˇā¸­ā¸ā¸šāš‰ā¸„ā¸ˇā¸™āš„ā¸Ÿā¸ĨāšŒā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰āš‚ā¸”ā¸ĸā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´āš€ā¸Ąā¸ˇāšˆā¸­ā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸”ā¸ąā¸‡ā¸ā¸Ĩāšˆā¸˛ā¸§ā¸œāšˆā¸˛ā¸™āš€ā¸§āš‡ā¸š", "advanced_settings_sync_remote_deletions_title": "ā¸‹ā¸´ā¸‡ā¸āšŒā¸ā¸˛ā¸Ŗā¸Ĩ⏚⏈⏞⏁⏪⏰ā¸ĸā¸°āš„ā¸ā¸Ĩ [⏄⏏⏓ā¸Ēā¸Ąā¸šā¸ąā¸•ā¸´ā¸—ā¸”ā¸Ĩ⏭⏇]", "advanced_settings_tile_subtitle": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ā¸‚ā¸ąāš‰ā¸™ā¸Ēā¸šā¸‡", @@ -401,6 +404,9 @@ "album_with_link_access": "ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āšƒā¸Ģāš‰ā¸—ā¸¸ā¸ā¸„ā¸™ā¸—ā¸ĩāšˆā¸Ąā¸ĩā¸Ĩā¸´ā¸‡ā¸āšŒā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸”ā¸šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩā¸°ā¸œā¸šāš‰ā¸„ā¸™ā¸—ā¸ĩāšˆā¸­ā¸ĸā¸šāšˆāšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸™ā¸ĩāš‰", "albums": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "albums_count": "{count, plural, one {{count, number} ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą} other {{count, number} ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą}}", + "albums_default_sort_order": "ā¸ā¸˛ā¸Ŗā¸ˆā¸ąā¸”āš€ā¸Ŗā¸ĩā¸ĸā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™", + "albums_default_sort_order_description": "ā¸ā¸˛ā¸Ŗā¸ˆā¸ąā¸”āš€ā¸Ŗā¸ĩā¸ĸā¸‡āšā¸­ā¸Ēāš€ā¸‹āš‡ā¸•āš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™āš€ā¸Ąā¸ˇāšˆā¸­ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšƒā¸Ģā¸Ąāšˆ", + "albums_feature_description": "⏁ā¸Ĩā¸¸āšˆā¸Ąā¸‚ā¸­ā¸‡āšā¸­ā¸Ēāš€ā¸‹āš‡ā¸•ā¸—ā¸ĩāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ēāšˆā¸‡āšƒā¸Ģāš‰ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸­ā¸ˇāšˆā¸™āš„ā¸”āš‰", "all": "ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "all_albums": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "all_people": "⏗⏏⏁⏄⏙", @@ -413,8 +419,8 @@ "anti_clockwise": "ā¸—ā¸§ā¸™āš€ā¸‚āš‡ā¸Ąā¸™ā¸˛ā¸Ŧ⏴⏁⏞", "api_key": "API key", "api_key_description": "ā¸„āšˆā¸˛ā¸™ā¸ĩāš‰ā¸ˆā¸°āšā¸Ēā¸”ā¸‡āš€ā¸žā¸ĩā¸ĸā¸‡ā¸„ā¸Ŗā¸ąāš‰ā¸‡āš€ā¸”ā¸ĩā¸ĸ⏧ āš‚ā¸›ā¸Ŗā¸”ā¸„ā¸ąā¸”ā¸Ĩā¸­ā¸ā¸āšˆā¸­ā¸™ā¸›ā¸´ā¸”ā¸Ģā¸™āš‰ā¸˛ā¸•āšˆā¸˛ā¸‡", - "api_key_empty": "ā¸Šā¸ˇāšˆā¸­ā¸„ā¸ĩā¸ĸāšŒ API ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš„ā¸Ąāšˆā¸„ā¸§ā¸Ŗā¸§āšˆā¸˛ā¸‡āš€ā¸›ā¸Ĩāšˆā¸˛", - "api_keys": "API ⏄ā¸ĩā¸ĸāšŒ", + "api_key_empty": "ā¸Šā¸ˇāšˆā¸­ API Key ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš„ā¸Ąāšˆā¸„ā¸§ā¸Ŗā¸§āšˆā¸˛ā¸‡āš€ā¸›ā¸Ĩāšˆā¸˛", + "api_keys": "API Key", "app_bar_signout_dialog_content": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸­ā¸ĸ⏞⏁⏭⏭⏁⏈⏞⏁⏪⏰⏚⏚", "app_bar_signout_dialog_ok": "āšƒā¸Šāšˆ", "app_bar_signout_dialog_title": "⏭⏭⏁⏈⏞⏁⏪⏰⏚⏚", @@ -428,7 +434,7 @@ "archive_size_description": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸‚ā¸™ā¸˛ā¸”ā¸Ēā¸šā¸‡ā¸Ē⏏⏔ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ (GiB)", "archived": "āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗāšā¸Ĩāš‰ā¸§", "archived_count": "{count, plural, other {āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ # ⏪⏞ā¸ĸ⏁⏞⏪}}", - "are_these_the_same_person": "āš€ā¸›āš‡ā¸™ā¸„ā¸™āš€ā¸”ā¸ĩā¸ĸā¸§ā¸ā¸ąā¸™ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", + "are_these_the_same_person": "āš€ā¸›āš‡ā¸™ā¸šā¸¸ā¸„ā¸„ā¸Ĩāš€ā¸”ā¸ĩā¸ĸā¸§ā¸ā¸ąā¸™ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", "are_you_sure_to_do_this": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸—ā¸ŗā¸Ēā¸´āšˆā¸‡ā¸™ā¸ĩāš‰ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", "asset_action_delete_err_read_only": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšā¸šā¸šā¸­āšˆā¸˛ā¸™ā¸­ā¸ĸāšˆā¸˛ā¸‡āš€ā¸”ā¸ĩā¸ĸā¸§āš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "asset_action_share_err_offline": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸”ā¸ļā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", @@ -448,24 +454,41 @@ "asset_list_settings_title": "ā¸•ā¸˛ā¸Ŗā¸˛ā¸‡ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", "asset_offline": "ā¸Ēā¸ˇāšˆā¸­ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒ", "asset_offline_description": "āš„ā¸Ąāšˆā¸žā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪⏠⏞ā¸ĸ⏙⏭⏁⏙ā¸ĩāš‰āšƒā¸™ā¸”ā¸´ā¸Ēā¸āšŒā¸­ā¸ĩā¸ā¸•āšˆā¸­āš„ā¸› āš‚ā¸›ā¸Ŗā¸”ā¸•ā¸´ā¸”ā¸•āšˆā¸­ā¸œā¸šāš‰ā¸”ā¸šāšā¸Ĩ⏪⏰⏚⏚ Immich ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš€ā¸žā¸ˇāšˆā¸­ā¸‚ā¸­ā¸„ā¸§ā¸˛ā¸Ąā¸Šāšˆā¸§ā¸ĸāš€ā¸Ģā¸Ĩ⏎⏭", + "asset_restored_successfully": "ā¸ā¸šāš‰ā¸„ā¸ˇā¸™ā¸Ēā¸ˇāšˆā¸­ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "asset_skipped": "ā¸‚āš‰ā¸˛ā¸Ąāšā¸Ĩāš‰ā¸§", "asset_skipped_in_trash": "āšƒā¸™ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰", "asset_uploaded": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āšā¸Ĩāš‰ā¸§", "asset_uploading": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔â€Ļ", + "asset_viewer_settings_subtitle": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗāšā¸Ēā¸”ā¸‡āšā¸ā¸Ĩāš€ā¸Ĩ⏭⏪ā¸ĩ", "asset_viewer_settings_title": "ā¸•ā¸ąā¸§ā¸”ā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪", "assets": "ā¸Ēā¸ˇāšˆā¸­", + "assets_added_count": "āš€ā¸žā¸´āšˆā¸Ą {count, plural, one{# ā¸Ēā¸ˇāšˆā¸­} other {# ā¸Ēā¸ˇāšˆā¸­}} āšā¸Ĩāš‰ā¸§", "assets_added_to_album_count": "āš€ā¸žā¸´āšˆā¸Ą {count, plural, one {# asset} other {# assets}} āš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", - "assets_added_to_name_count": "āš€ā¸žā¸´āšˆā¸Ą {count, plural, one {# asset} other {# assets}} āš„ā¸›ā¸ĸā¸ąā¸‡ {hasName, select, true {{name}} other {new album}}", + "assets_cannot_be_added_to_album_count": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸žā¸´āšˆā¸Ą {count, plural, one {ā¸Ēā¸ˇāšˆā¸­} other {ā¸Ēā¸ˇāšˆā¸­}} āš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", + "assets_count": "{count, plural, one { ā¸Ēā¸ˇāšˆā¸­} other { ā¸Ēā¸ˇāšˆā¸­}}", + "assets_deleted_permanently": "{count} ā¸Ēā¸ˇāšˆā¸­ā¸–ā¸šā¸ā¸Ĩ⏚⏭ā¸ĸāšˆā¸˛ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ", + "assets_deleted_permanently_from_server": "ā¸Ĩ⏚ {count} ā¸Ēā¸ˇāšˆā¸­ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ Immich ⏭ā¸ĸāšˆā¸˛ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ", + "assets_downloaded_failed": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ {count, plural, one {āš„ā¸Ÿā¸ĨāšŒ} other {āš„ā¸Ÿā¸ĨāšŒ}} āš„ā¸Ąāšˆā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ - {error}", + "assets_downloaded_successfully": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ {count, plural, one {āš„ā¸Ÿā¸ĨāšŒ} other {āš„ā¸Ÿā¸ĨāšŒ}} ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "assets_moved_to_trash_count": "ā¸ĸāš‰ā¸˛ā¸ĸ {count, plural, one {# asset} other {# assets}} āš„ā¸›ā¸ĸā¸ąā¸‡ā¸–ā¸ąā¸‡ā¸‚ā¸ĸā¸°āšā¸Ĩāš‰ā¸§", "assets_permanently_deleted_count": "ā¸Ĩ⏚ {count, plural, one {# asset} other {# assets}} ā¸—ā¸´āš‰ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ", "assets_removed_count": "{count, plural, one {# asset} other {# assets}} ā¸–ā¸šā¸ā¸Ĩā¸šāšā¸Ĩāš‰ā¸§", + "assets_removed_permanently_from_device": "⏙⏺ {count} ā¸Ēā¸ˇāšˆā¸­ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ", "assets_restore_confirmation": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸ā¸šāš‰ā¸„ā¸ˇā¸™ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸—ā¸´āš‰ā¸‡ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”? ā¸„ā¸¸ā¸“āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ĸāš‰ā¸­ā¸™ā¸ā¸Ĩā¸ąā¸šā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸™ā¸ĩāš‰āš„ā¸”āš‰! āš‚ā¸›ā¸Ŗā¸”ā¸—ā¸Ŗā¸˛ā¸šā¸§āšˆā¸˛ā¸Ēā¸ˇāšˆā¸­ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒāšƒā¸”āš† āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ā¸šāš‰ā¸„ā¸ˇā¸™āš„ā¸”āš‰ā¸”āš‰ā¸§ā¸ĸ⏧⏴⏘ā¸ĩ⏙ā¸ĩāš‰", "assets_restored_count": "{count, plural, one {# asset} other {# assets}} ā¸„ā¸ˇā¸™ā¸„āšˆā¸˛", + "assets_restored_successfully": "ā¸ā¸šāš‰ā¸„ā¸ˇā¸™ {count} ā¸Ēā¸ˇāšˆā¸­ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", + "assets_trashed": "ā¸ĸāš‰ā¸˛ā¸ĸ {count} ā¸Ēā¸ˇāšˆā¸­āš„ā¸›ā¸ĸā¸ąā¸‡ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰", "assets_trashed_count": "{count, plural, one {# asset} other {# assets}} ā¸–ā¸šā¸ā¸Ĩ⏚", + "assets_trashed_from_server": "ā¸ĸāš‰ā¸˛ā¸ĸ {count} ā¸Ēā¸ˇāšˆā¸­ā¸ˆā¸˛ā¸ Immich āš„ā¸›ā¸ĸā¸ąā¸‡ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ⏭ā¸ĸā¸šāšˆāšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸­ā¸ĸā¸šāšˆāšā¸Ĩāš‰ā¸§", "authorized_devices": "ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸—ā¸ĩāšˆāš„ā¸”āš‰ā¸Ŗā¸ąā¸šā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•", + "automatic_endpoint_switching_subtitle": "āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸”āš‰ā¸§ā¸ĸ LAN ⏠⏞ā¸ĸāšƒā¸™ā¸§ā¸‡ Wi-Fi ⏗ā¸ĩāšˆā¸Ŗā¸°ā¸šā¸¸āš„ā¸§āš‰ āšā¸Ĩā¸°āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸”āš‰ā¸§ā¸ĸ⏧⏴⏘ā¸ĩā¸­ā¸ˇāšˆā¸™āš€ā¸Ąā¸ˇāšˆā¸­ā¸­ā¸ĸā¸šāšˆā¸™ā¸­ā¸ Wi-Fi ⏗ā¸ĩāšˆā¸Ŗā¸°ā¸šā¸¸āš„ā¸§āš‰", + "automatic_endpoint_switching_title": "ā¸Ēā¸Ĩā¸ąā¸š URL ā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´", + "autoplay_slideshow": "āš€ā¸Ĩāšˆā¸™ā¸Ēāš„ā¸Ĩā¸”āšŒāš‚ā¸Šā¸§āšŒ", "back": "⏁ā¸Ĩā¸ąā¸š", "back_close_deselect": "ā¸ĸāš‰ā¸­ā¸™ā¸ā¸Ĩā¸ąā¸š, ⏛⏴⏔, ā¸Ģ⏪⏎⏭ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗāš€ā¸Ĩ⏎⏭⏁", + "background_location_permission": "ā¸ā¸˛ā¸Ŗā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•ā¸Ŗā¸°ā¸šā¸¸ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡ā¸žā¸ˇāš‰ā¸™ā¸Ģā¸Ĩā¸ąā¸‡", + "background_location_permission_content": "āš€ā¸žā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸ˆā¸°ā¸Ēā¸Ĩā¸ąā¸šā¸ā¸˛ā¸Ŗāš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸‚ā¸“ā¸°ā¸—ā¸ĩāšˆā¸Ŗā¸ąā¸™āšƒā¸™ā¸žā¸ˇāš‰ā¸™ā¸Ģā¸Ĩā¸ąā¸‡ Immich ā¸•āš‰ā¸­ā¸‡ā¸Ŗā¸šāš‰ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡ā¸—ā¸ĩāšˆāšā¸Ąāšˆā¸ĸ⏺⏕ā¸Ĩā¸­ā¸”āš€ā¸§ā¸Ĩ⏞ āš€ā¸žā¸ˇāšˆā¸­ā¸ˆā¸°ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸­āšˆā¸˛ā¸™ā¸Šā¸ˇāšˆā¸­ Wi-Fi", "backup_album_selection_page_albums_device": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸šā¸™āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ ({count})", "backup_album_selection_page_albums_tap": "ā¸ā¸”āš€ā¸žā¸ˇāšˆā¸­ā¸Ŗā¸§ā¸Ą ⏁⏔ā¸Ēā¸­ā¸‡ā¸„ā¸Ŗā¸ąāš‰ā¸‡āš€ā¸žā¸ˇāšˆā¸­ā¸ĸā¸āš€ā¸§āš‰ā¸™", "backup_album_selection_page_assets_scatter": "ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏞⏪ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ā¸Ŗā¸°ā¸ˆā¸˛ā¸ĸāš„ā¸›āšƒā¸™ā¸Ģā¸Ĩ⏞ā¸ĸā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą ā¸”ā¸ąā¸‡ā¸™ā¸ąāš‰ā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸–ā¸šā¸ā¸Ŗā¸§ā¸Ąā¸Ģ⏪⏎⏭ā¸ĸā¸āš€ā¸§āš‰ā¸™āšƒā¸™ā¸ā¸Ŗā¸°ā¸šā¸§ā¸™ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", @@ -496,15 +519,16 @@ "backup_controller_page_background_is_on": "⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´āš€ā¸›ā¸´ā¸”ā¸­ā¸ĸā¸šāšˆ", "backup_controller_page_background_turn_off": "ā¸›ā¸´ā¸”ā¸šā¸Ŗā¸´ā¸ā¸˛ā¸Ŗāš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡", "backup_controller_page_background_turn_on": "āš€ā¸›ā¸´ā¸”ā¸šā¸Ŗā¸´ā¸ā¸˛ā¸Ŗāš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡", - "backup_controller_page_background_wifi": "ā¸šā¸™ WiFi āš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™", + "backup_controller_page_background_wifi": "ā¸šā¸™ Wi-Fi āš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™", "backup_controller_page_backup": "ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "backup_controller_page_backup_selected": "⏗ā¸ĩāšˆāš€ā¸Ĩ⏎⏭⏁: ", "backup_controller_page_backup_sub": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸Ēā¸ŗā¸Ŗā¸­ā¸‡āšā¸Ĩāš‰ā¸§", "backup_controller_page_created": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āš€ā¸Ąā¸ˇāšˆā¸­: {date}", "backup_controller_page_desc_backup": "āš€ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™ā¸‰ā¸˛ā¸ā¸Ģā¸™āš‰ā¸˛āš€ā¸žā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸ˆā¸°ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩā¸”ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšƒā¸Ģā¸Ąāšˆāš„ā¸›ā¸ĸā¸ąā¸‡āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒāš€ā¸Ąā¸ˇāšˆā¸­āš€ā¸›ā¸´ā¸”āšā¸­ā¸ž", - "backup_controller_page_excluded": "ā¸–ā¸šā¸ā¸ĸā¸āš€ā¸§āš‰ā¸™: ", + "backup_controller_page_excluded": "ā¸ĸā¸āš€ā¸§āš‰ā¸™: ", "backup_controller_page_failed": "ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧ ({count})", "backup_controller_page_filename": "ā¸Šā¸ˇāšˆā¸­āš„ā¸Ÿā¸ĨāšŒ: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš€ā¸ā¸ĩāšˆā¸ĸā¸§ā¸ā¸ąā¸šā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "backup_controller_page_none_selected": "āš„ā¸Ąāšˆā¸Ąā¸ĩ⏗ā¸ĩāšˆāš€ā¸Ĩ⏎⏭⏁", "backup_controller_page_remainder": "⏗ā¸ĩāšˆāš€ā¸Ģā¸Ĩ⏎⏭", @@ -526,7 +550,12 @@ "backup_manual_success": "ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "backup_manual_title": "ā¸Ēā¸–ā¸˛ā¸™ā¸°ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩ⏔", "backup_options_page_title": "ā¸•ā¸ąā¸§āš€ā¸Ĩ⏎⏭⏁⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", + "backup_setting_subtitle": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩā¸”āšƒā¸™ā¸‰ā¸˛ā¸ā¸Ģā¸™āš‰ā¸˛ āšā¸Ĩā¸°ā¸žā¸ˇāš‰ā¸™ā¸Ģā¸Ĩā¸ąā¸‡", "backward": "⏁ā¸Ĩā¸ąā¸šā¸Ģā¸Ĩā¸ąā¸‡", + "biometric_auth_enabled": "ā¸ā¸˛ā¸Ŗā¸žā¸´ā¸Ēā¸šā¸ˆā¸™āšŒā¸­ā¸ąā¸•ā¸Ĩā¸ąā¸ā¸Šā¸“āšŒāš€ā¸žā¸ˇāšˆā¸­ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸•ā¸ąā¸§ā¸šā¸¸ā¸„ā¸„ā¸Ĩā¸–ā¸šā¸āš€ā¸›ā¸´ā¸”", + "biometric_locked_out": "ā¸ā¸˛ā¸Ŗā¸žā¸´ā¸Ēā¸šā¸ˆā¸™āšŒā¸­ā¸ąā¸•ā¸Ĩā¸ąā¸ā¸Šā¸“āšŒāš€ā¸žā¸ˇāšˆā¸­ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸•ā¸ąā¸§ā¸šā¸¸ā¸„ā¸„ā¸Ĩā¸–ā¸šā¸ā¸Ĩāš‡ā¸­ā¸„", + "biometric_no_options": "āš„ā¸Ąāšˆā¸Ąā¸ĩā¸•ā¸ąā¸§āš€ā¸Ĩā¸ˇā¸­ā¸ā¸ā¸˛ā¸Ŗā¸žā¸´ā¸Ēā¸šā¸ˆā¸™āšŒā¸­ā¸ąā¸•ā¸Ĩā¸ąā¸ā¸Šā¸“āšŒāš€ā¸žā¸ˇāšˆā¸­ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸•ā¸ąā¸§ā¸šā¸¸ā¸„ā¸„ā¸Ĩ", + "biometric_not_available": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ā¸ā¸˛ā¸Ŗā¸žā¸´ā¸Ēā¸šā¸ˆā¸™āšŒā¸­ā¸ąā¸•ā¸Ĩā¸ąā¸ā¸Šā¸“āšŒāš€ā¸žā¸ˇāšˆā¸­ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸•ā¸ąā¸§ā¸šā¸¸ā¸„ā¸„ā¸Ĩāš„ā¸”āš‰ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰", "birthdate_saved": "ā¸šā¸ąā¸™ā¸—ā¸ļā¸ā¸§ā¸ąā¸™āš€ā¸ā¸´ā¸”āšā¸Ĩāš‰ā¸§", "birthdate_set_description": "ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆāš€ā¸ā¸´ā¸”ā¸ˆā¸°ā¸™ā¸ŗā¸Ąā¸˛āšƒā¸Šāš‰āšƒā¸™ā¸ā¸˛ā¸Ŗā¸„ā¸ŗā¸™ā¸§ā¸“ā¸­ā¸˛ā¸ĸā¸¸ā¸‚ā¸­ā¸‡ā¸šā¸¸ā¸„ā¸„ā¸Ĩ⏙ā¸ĩāš‰āšƒā¸™ā¸‚ā¸“ā¸°ā¸—ā¸ĩāšˆā¸–āšˆā¸˛ā¸ĸā¸Ŗā¸šā¸›", "blurred_background": "ā¸žā¸ˇāš‰ā¸™ā¸Ģā¸Ĩā¸ąā¸‡āšā¸šā¸šāš€ā¸šā¸Ĩ⏭", @@ -556,14 +585,19 @@ "camera_model": "ā¸Ŗā¸¸āšˆā¸™ā¸ā¸Ĩāš‰ā¸­ā¸‡", "cancel": "ā¸ĸā¸āš€ā¸Ĩ⏴⏁", "cancel_search": "ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞", + "canceled": "ā¸ĸā¸āš€ā¸Ĩ⏴⏁", "cannot_merge_people": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ŗā¸§ā¸Ąā¸ā¸Ĩā¸¸āšˆā¸Ąā¸„ā¸™āš„ā¸”āš‰", "cannot_undo_this_action": "⏁⏞⏪⏁⏪⏰⏗⏺⏙ā¸ĩāš‰āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ĸāš‰ā¸­ā¸™ā¸ā¸Ĩā¸ąā¸šāš„ā¸”āš‰!", "cannot_update_the_description": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸­ā¸ąā¸žāš€ā¸”ā¸—ā¸Ŗā¸˛ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸā¸”āš„ā¸”āš‰", + "cast": "āšā¸„ā¸Ēā¸•āšŒ", + "cast_description": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸›ā¸Ĩ⏞ā¸ĸā¸—ā¸˛ā¸‡āšā¸„ā¸Ēā¸•āšŒ", "change_date": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ", + "change_description": "āšā¸āš‰āš„ā¸‚ā¸„ā¸ŗā¸­ā¸˜ā¸´ā¸šā¸˛ā¸ĸ", + "change_display_order": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙ā¸Ĩā¸ŗā¸”ā¸ąā¸šā¸ā¸˛ā¸Ŗāšā¸Ēā¸”ā¸‡ā¸œā¸Ĩ", "change_expiration_time": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āš€ā¸§ā¸Ĩ⏞ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸ⏏", "change_location": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸•āšā¸˛āšā¸Ģā¸™āšˆā¸‡", "change_name": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸Šā¸ˇāšˆā¸­", - "change_name_successfully": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸Šā¸ˇāšˆā¸­āš€ā¸Ŗā¸ĩā¸ĸā¸šā¸Ŗāš‰ā¸­ā¸ĸāšā¸Ĩāš‰ā¸§", + "change_name_successfully": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸Šā¸ˇāšˆā¸­ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "change_password": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™", "change_password_description": "ā¸ā¸˛ā¸Ŗāš€ā¸‚āš‰ā¸˛ā¸Ēā¸šāšˆā¸Ŗā¸°ā¸šā¸šā¸„ā¸Ŗā¸ąāš‰ā¸‡āšā¸Ŗā¸ ā¸ˆā¸ŗāš€ā¸›āš‡ā¸™ā¸ˆā¸•āš‰ā¸­ā¸‡āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš€ā¸žā¸ˇāšˆā¸­ā¸„ā¸§ā¸˛ā¸Ąā¸›ā¸Ĩā¸­ā¸”ā¸ ā¸ąā¸ĸ āš‚ā¸›ā¸Ŗā¸”ā¸›āš‰ā¸­ā¸™ā¸Ŗā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āšƒā¸Ģā¸Ąāšˆā¸”āš‰ā¸˛ā¸™ā¸Ĩāšˆā¸˛ā¸‡", "change_password_form_confirm_password": "ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸Ŗā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™", @@ -574,6 +608,9 @@ "change_pin_code": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸›ā¸Ŗā¸°ā¸ˆā¸ŗā¸•ā¸ąā¸§ (PIN)", "change_your_password": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", "changed_visibility_successfully": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸ā¸˛ā¸Ŗā¸Ąā¸­ā¸‡āš€ā¸Ģāš‡ā¸™āš€ā¸Ŗā¸ĩā¸ĸā¸šā¸Ŗāš‰ā¸­ā¸ĸāšā¸Ĩāš‰ā¸§", + "check_corrupt_asset_backup": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ē⏭⏚ā¸Ē⏺⏪⏭⏇ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸œā¸´ā¸”ā¸›ā¸ā¸•ā¸´", + "check_corrupt_asset_backup_button": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ē⏭⏚", + "check_corrupt_asset_backup_description": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šāš€ā¸Ąā¸ˇāšˆā¸­āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ Wi-Fi āšā¸Ĩ⏰ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸–ā¸šā¸ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšā¸Ĩāš‰ā¸§āš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™ ā¸ā¸˛ā¸Ŗā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šā¸­ā¸˛ā¸ˆāšƒā¸Šāš‰āš€ā¸§ā¸Ĩ⏞ā¸Ģā¸Ĩ⏞ā¸ĸ⏙⏞⏗ā¸ĩ", "check_logs": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šā¸šā¸ąā¸™ā¸—ā¸ļ⏁", "choose_matching_people_to_merge": "āš€ā¸Ĩ⏎⏭⏁⏄⏙⏗ā¸ĩāšˆā¸•ā¸Ŗā¸‡ā¸ā¸ąā¸™āš€ā¸žā¸ˇāšˆā¸­ā¸Ŗā¸§ā¸Ąāš€ā¸‚āš‰ā¸˛ā¸”āš‰ā¸§ā¸ĸā¸ā¸ąā¸™", "city": "āš€ā¸Ąā¸ˇā¸­ā¸‡", @@ -582,6 +619,14 @@ "clear_all_recent_searches": "ā¸Ĩāš‰ā¸˛ā¸‡ā¸›ā¸Ŗā¸°ā¸§ā¸ąā¸•ā¸´ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞", "clear_message": "ā¸Ĩāš‰ā¸˛ā¸‡ā¸‚āš‰ā¸­ā¸„ā¸§ā¸˛ā¸Ą", "clear_value": "ā¸Ĩāš‰ā¸˛ā¸‡ā¸„āšˆā¸˛", + "client_cert_dialog_msg_confirm": "āš€ā¸Ēā¸Ŗāš‡ā¸ˆ", + "client_cert_enter_password": "āšƒā¸Ēāšˆā¸Ŗā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™", + "client_cert_import": "ā¸™ā¸ŗāš€ā¸‚āš‰ā¸˛", + "client_cert_import_success_msg": "ā¸™ā¸ŗāš€ā¸‚āš‰ā¸˛āšƒā¸šā¸Ŗā¸ąā¸šā¸Ŗā¸­ā¸‡ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", + "client_cert_invalid_msg": "āšƒā¸šā¸Ŗā¸ąā¸šā¸Ŗā¸­ā¸‡ ā¸Ģ⏪⏎⏭⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āš„ā¸Ąāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡", + "client_cert_remove_msg": "ā¸Ĩā¸šāšƒā¸šā¸Ŗā¸ąā¸šā¸Ŗā¸­ā¸‡ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", + "client_cert_subtitle": "ā¸Ŗā¸­ā¸‡ā¸Ŗā¸ąā¸šāš€ā¸‰ā¸žā¸˛ā¸° PKCS12 (.p12, .pfx) āš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™ ā¸ā¸˛ā¸Ŗā¸™ā¸ŗāš€ā¸‚āš‰ā¸˛/ā¸Ĩā¸šāšƒā¸šā¸Ŗā¸ąā¸šā¸Ŗā¸­ā¸‡ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸—ā¸ŗāš„ā¸”āš‰ā¸āšˆā¸­ā¸™ā¸Ĩāš‡ā¸­ā¸„ā¸­ā¸´ā¸™āš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™", + "client_cert_title": "āšƒā¸šā¸Ŗā¸ąā¸šā¸Ŗā¸­ā¸‡ SSL āš„ā¸„ā¸Ĩāš€ā¸­ā¸™ā¸•āšŒ", "clockwise": "ā¸•ā¸˛ā¸Ąāš€ā¸‚āš‡ā¸Ąā¸™ā¸˛ā¸Ŧ⏴⏁⏞", "close": "⏛⏴⏔", "collapse": "ā¸ĸāšˆā¸­", @@ -602,6 +647,10 @@ "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": "ā¸Ąā¸ĩ⏭ā¸ĸā¸šāšˆ", "context": "ā¸šā¸Ŗā¸´ā¸šā¸—", "continue": "ā¸•āšˆā¸­āš„ā¸›", @@ -610,6 +659,7 @@ "control_bottom_app_bar_delete_from_local": "ā¸Ĩā¸šā¸ˆā¸˛ā¸āš€ā¸Ŗā¸ˇāšˆā¸­ā¸‡", "control_bottom_app_bar_edit_location": "āšā¸āš‰āš„ā¸‚ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡", "control_bottom_app_bar_edit_time": "āšā¸āš‰āš„ā¸‚ā¸§ā¸ąā¸™āšā¸Ĩā¸°āš€ā¸§ā¸Ĩ⏞", + "control_bottom_app_bar_share_link": "āšā¸Šā¸ŖāšŒā¸Ĩā¸´ā¸‡ā¸„āšŒ", "control_bottom_app_bar_share_to": "āšā¸Šā¸ŖāšŒāšƒā¸Ģāš‰", "control_bottom_app_bar_trash_from_immich": "ā¸ĸāš‰ā¸˛ā¸ĸāš€ā¸‚āš‰ā¸˛ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰", "copied_image_to_clipboard": "ā¸„ā¸ąā¸”ā¸Ĩā¸­ā¸ā¸ ā¸˛ā¸žāš„ā¸›ā¸ĸā¸ąā¸‡ā¸„ā¸Ĩā¸´ā¸›ā¸šā¸­ā¸ŖāšŒā¸”āšā¸Ĩāš‰ā¸§", @@ -631,6 +681,7 @@ "create_link": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸Ĩā¸´ā¸‡ā¸āšŒ", "create_link_to_share": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸Ĩā¸´ā¸‡ā¸āšŒāš€ā¸žā¸ˇāšˆā¸­āšā¸Šā¸ŖāšŒ", "create_link_to_share_description": "ā¸œā¸šāš‰ā¸—ā¸ĩāšˆā¸Ąā¸ĩā¸Ĩā¸´ā¸‡ā¸āšŒ ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸”ā¸šā¸Ŗā¸šā¸›ā¸—ā¸ĩāšˆāš€ā¸Ĩā¸ˇā¸­ā¸āš„ā¸”āš‰", + "create_new": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšƒā¸Ģā¸Ąāšˆ", "create_new_person": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸„ā¸™āšƒā¸Ģā¸Ąāšˆ", "create_new_person_hint": "⏁⏺ā¸Ģ⏙⏔ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆāš€ā¸Ĩā¸ˇā¸­ā¸āšƒā¸Ģāš‰ā¸ā¸ąā¸šā¸„ā¸™āšƒā¸Ģā¸Ąāšˆ", "create_new_user": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āšƒā¸Ģā¸Ąāšˆ", @@ -640,9 +691,12 @@ "create_tag_description": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšā¸—āš‡ā¸āšƒā¸Ģā¸Ąāšˆ ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šāšā¸—āš‡ā¸ā¸—ā¸ĩāšˆā¸‹āš‰ā¸­ā¸™ā¸ā¸ąā¸™ āš‚ā¸›ā¸Ŗā¸”ā¸›āš‰ā¸­ā¸™āš€ā¸Ēāš‰ā¸™ā¸—ā¸˛ā¸‡ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸‚ā¸­ā¸‡āšā¸—āš‡ā¸ ā¸Ŗā¸§ā¸Ąā¸–ā¸ļā¸‡āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ā¸Ģā¸Ąā¸˛ā¸ĸā¸—ā¸ąā¸š", "create_user": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸œā¸šāš‰āšƒā¸Šāš‰", "created": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšā¸Ĩāš‰ā¸§", + "created_at": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āš€ā¸Ąā¸ˇāšˆā¸­", + "crop": "⏄⏪⏭⏛", "curated_object_page_title": "ā¸Ēā¸´āšˆā¸‡ā¸‚ā¸­ā¸‡", "current_device": "ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸›ā¸ąā¸ˆā¸ˆā¸¸ā¸šā¸ąā¸™", "current_pin_code": "⏪ā¸Ģā¸ąā¸Ēā¸›ā¸Ŗā¸°ā¸ˆā¸ŗā¸•ā¸ąā¸§ (PIN) ā¸›ā¸ąā¸ˆā¸ˆā¸¸ā¸šā¸ąā¸™", + "current_server_address": "⏗ā¸ĩāšˆā¸­ā¸ĸā¸šāšˆāš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸›ā¸ąā¸ˆā¸ˆā¸¸ā¸šā¸ąā¸™", "custom_locale": "ā¸›ā¸Ŗā¸ąā¸šā¸ ā¸˛ā¸Šā¸˛ā¸—āš‰ā¸­ā¸‡ā¸–ā¸´āšˆā¸™āš€ā¸­ā¸‡", "custom_locale_description": "āšƒā¸Šāš‰ā¸Ŗā¸šā¸›āšā¸šā¸šā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆāšā¸Ĩā¸°ā¸•ā¸ąā¸§āš€ā¸Ĩā¸‚ā¸ˆā¸˛ā¸ā¸ ā¸˛ā¸Šā¸˛āšā¸Ĩā¸°ā¸‚ā¸­ā¸šāš€ā¸‚ā¸•", "daily_title_text_date": "E dd MMM", @@ -694,6 +748,7 @@ "disallow_edits": "āš„ā¸Ąāšˆā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āšƒā¸Ģāš‰āšā¸āš‰āš„ā¸‚", "discord": "⏔⏴ā¸Ēā¸„ā¸­ā¸ŖāšŒā¸”", "discover": "ā¸„āš‰ā¸™ā¸žā¸š", + "discovered_devices": "ā¸„āš‰ā¸™ā¸Ģā¸˛ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒ", "dismiss_all_errors": "ā¸›ā¸ā¸´āš€ā¸Ēā¸˜ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩā¸˛ā¸”ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "dismiss_error": "ā¸›ā¸ā¸´āš€ā¸Ēā¸˜ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔", "display_options": "ā¸•ā¸ąā¸§āš€ā¸Ĩā¸ˇā¸­ā¸ā¸ā¸˛ā¸Ŗāšā¸Ē⏔⏇", @@ -704,12 +759,25 @@ "documentation": "āš€ā¸­ā¸ā¸Ē⏞⏪", "done": "ā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "download": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", + "download_canceled": "ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ā¸ĸā¸āš€ā¸Ĩ⏴⏁", + "download_complete": "ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”āš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸´āš‰ā¸™", + "download_enqueue": "ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔⏭ā¸ĸā¸šāšˆāšƒā¸™ā¸„ā¸´ā¸§", + "download_error": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔", + "download_failed": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”āš„ā¸Ąāšˆā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", + "download_finished": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”āš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸´āš‰ā¸™", "download_include_embedded_motion_videos": "ā¸Ŗā¸§ā¸Ąā¸§ā¸´ā¸”ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸ā¸ąā¸‡ā¸­ā¸ĸā¸šāšˆāšƒā¸™ā¸ ā¸˛ā¸žāš€ā¸„ā¸Ĩā¸ˇāšˆā¸­ā¸™āš„ā¸Ģ⏧", "download_include_embedded_motion_videos_description": "ā¸Ŗā¸§ā¸Ąā¸§ā¸´ā¸”ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸ā¸ąā¸‡ā¸­ā¸ĸā¸šāšˆāšƒā¸™ā¸ ā¸˛ā¸žāš€ā¸„ā¸Ĩā¸ˇāšˆā¸­ā¸™āš„ā¸Ģā¸§āš€ā¸Ąā¸ˇāšˆā¸­ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", + "download_notfound": "āš„ā¸Ąāšˆā¸žā¸šā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", + "download_paused": "ā¸Ģā¸ĸā¸¸ā¸”ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”ā¸Šā¸ąāšˆā¸§ā¸„ā¸Ŗā¸˛ā¸§", "download_settings": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", "download_settings_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", + "download_started": "āš€ā¸Ŗā¸´āšˆā¸Ąā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", + "download_sucess": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", + "download_sucess_android": "ā¸Ēā¸ˇāšˆā¸­ā¸–ā¸šā¸ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”āš„ā¸›ā¸ĸā¸ąā¸‡ DCIM/Immich", + "download_waiting_to_retry": "⏪⏭ā¸Ĩā¸­ā¸‡āšƒā¸Ģā¸Ąāšˆ", "downloading": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", "downloading_asset_filename": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ {filename}", + "downloading_media": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ˇāšˆā¸­", "drop_files_to_upload": "ā¸§ā¸˛ā¸‡āš„ā¸Ÿā¸ĨāšŒāšƒā¸™ā¸Šāšˆā¸­ā¸‡ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔", "duplicates": "⏪⏞ā¸ĸ⏁⏞⏪⏗ā¸ĩāšˆā¸‹āš‰ā¸ŗā¸ā¸ąā¸™", "duplicates_description": "āšā¸āš‰āš„ā¸‚āšā¸•āšˆā¸Ĩ⏰⏁ā¸Ĩā¸¸āšˆā¸Ąāš‚ā¸”ā¸ĸā¸Ŗā¸°ā¸šā¸¸ā¸§āšˆā¸˛ā¸ā¸Ĩā¸¸āšˆā¸Ąāšƒā¸”ā¸‹āš‰ā¸ŗā¸ā¸ąā¸™ā¸Ģā¸˛ā¸ā¸Ąā¸ĩ", @@ -719,6 +787,8 @@ "edit_avatar": "āšā¸āš‰āš„ā¸‚ā¸•ā¸ąā¸§ā¸Ĩ⏰⏄⏪", "edit_date": "āšā¸āš‰āš„ā¸‚ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ", "edit_date_and_time": "āšā¸āš‰āš„ā¸‚ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆāšā¸Ĩā¸°āš€ā¸§ā¸Ĩ⏞", + "edit_description": "āšā¸āš‰āš„ā¸‚ā¸„ā¸ŗā¸­ā¸˜ā¸´ā¸šā¸˛ā¸ĸ", + "edit_description_prompt": "āš‚ā¸›ā¸Ŗā¸”āš€ā¸Ĩā¸ˇāšˆā¸­ā¸ā¸„ā¸ŗā¸­ā¸˜ā¸´ā¸šā¸˛ā¸ĸāšƒā¸Ģā¸Ąāšˆ", "edit_exclusion_pattern": "āšā¸āš‰āš„ā¸‚ā¸‚āš‰ā¸­ā¸ĸā¸āš€ā¸§āš‰ā¸™", "edit_faces": "āšā¸āš‰āš„ā¸‚ā¸Ģā¸™āš‰ā¸˛", "edit_import_path": "āšā¸āš‰āš„ā¸‚ā¸žā¸˛ā¸˜ā¸™āšā¸˛āš€ā¸‚āš‰ā¸˛", @@ -739,15 +809,24 @@ "editor_crop_tool_h2_aspect_ratios": "ā¸­ā¸ąā¸•ā¸Ŗā¸˛ā¸Ēāšˆā¸§ā¸™ā¸ ā¸˛ā¸ž", "editor_crop_tool_h2_rotation": "⏁⏞⏪ā¸Ģā¸Ąā¸¸ā¸™", "email": "⏭ā¸ĩāš€ā¸Ąā¸Ĩ", + "email_notifications": "āšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™ā¸œāšˆā¸˛ā¸™ā¸­ā¸ĩāš€ā¸Ąā¸Ĩ", + "empty_folder": "āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸™ā¸ĩāš‰ā¸§āšˆā¸˛ā¸‡āš€ā¸›ā¸Ĩāšˆā¸˛", "empty_trash": "ā¸—ā¸´āš‰ā¸‡ā¸ˆā¸˛ā¸ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰", "empty_trash_confirmation": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩāš‰ā¸˛ā¸‡ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰ ā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸™ā¸ĩāš‰ā¸ˆā¸°ā¸Ĩā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āšƒā¸™ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰⏭⏭⏁⏈⏞⏁ Immich ⏭ā¸ĸāšˆā¸˛ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ\nā¸„ā¸¸ā¸“āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ĸāš‰ā¸­ā¸™ā¸ā¸Ĩā¸ąā¸šā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸™ā¸ĩāš‰āš„ā¸”āš‰!", "enable": "āš€ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", + "enable_biometric_auth_description": "āšƒā¸Ēāšˆā¸žā¸´ā¸™āš€ā¸žā¸ˇāšˆā¸­āš€ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗā¸žā¸´ā¸Ēā¸šā¸ˆā¸™āšŒā¸­ā¸ąā¸•ā¸Ĩā¸ąā¸ā¸Šā¸“āšŒāš€ā¸žā¸ˇāšˆā¸­ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸•ā¸ąā¸§ā¸šā¸¸ā¸„ā¸„ā¸Ĩ", "enabled": "āš€ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "end_date": "ā¸§ā¸ąā¸™ā¸Ēā¸´āš‰ā¸™ā¸Ē⏏⏔", - "enter_wifi_name": "Enter WiFi name", + "enqueued": "⏪⏭⏄⏴⏧", + "enter_wifi_name": "āšƒā¸Ēāšˆā¸Šā¸ˇāšˆā¸­ Wi-Fi", + "enter_your_pin_code": "āšƒā¸Ēāšˆā¸žā¸´ā¸™āš‚ā¸„āš‰ā¸”", + "enter_your_pin_code_subtitle": "āšƒā¸Ēāšˆā¸žā¸´ā¸™āš‚ā¸„āš‰ā¸”āš€ā¸žā¸ˇāšˆā¸­āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ĩāš‡ā¸­ā¸„", "error": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔", + "error_change_sort_album": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸ā¸˛ā¸Ŗāš€ā¸Ŗā¸ĩā¸ĸ⏇ā¸Ĩā¸ŗā¸”ā¸ąā¸šā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāš„ā¸Ąāšˆā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "error_delete_face": "āš€ā¸ā¸´ā¸”āš€ā¸­ā¸­āš€ā¸Ŗā¸­ā¸ŖāšŒ āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸šāšƒā¸šā¸Ģā¸™āš‰ā¸˛ā¸­ā¸­ā¸āš„ā¸”āš‰", "error_loading_image": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔⏪⏰ā¸Ģā¸§āšˆā¸˛ā¸‡āš‚ā¸Ģā¸Ĩā¸”ā¸ ā¸˛ā¸ž", + "error_saving_image": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔⏪⏰ā¸Ģā¸§āšˆā¸˛ā¸‡āš€ā¸‹ā¸Ÿā¸ ā¸˛ā¸ž: {error}", + "error_tag_face_bounding_box": "ā¸ā¸˛ā¸Ŗāšā¸—āš‡ā¸āšƒā¸šā¸Ģā¸™āš‰ā¸˛ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔ - āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸•ā¸ĩā¸ā¸Ŗā¸­ā¸šāšƒā¸šā¸Ģā¸™āš‰ā¸˛āš„ā¸”āš‰", "error_title": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔", "errors": { "cannot_navigate_next_asset": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āš€ā¸Ēāš‰ā¸™ā¸—ā¸˛ā¸‡āš„ā¸”āš‰", @@ -755,7 +834,7 @@ "cant_apply_changes": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩā¸˛ā¸”āšƒā¸™ā¸ā¸˛ā¸Ŗāš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸›ā¸Ĩ⏇", "cant_change_activity": "Can't {enabled, select, true {disable} other {enable}} activity", "cant_change_asset_favorite": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸Šā¸ˇāšˆā¸™ā¸Šā¸­ā¸šāš„ā¸”āš‰", - "cant_change_metadata_assets_count": "Can't change metadata of {count, plural, one {# asset} other {# assets}}", + "cant_change_metadata_assets_count": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āšā¸āš‰āš„ā¸‚ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ metadata ⏂⏭⏇ {count, plural, one {# ā¸Ēā¸ˇāšˆā¸­} other {# ā¸Ēā¸ˇāšˆā¸­}}", "cant_get_faces": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩā¸˛ā¸”āšƒā¸™ā¸ā¸˛ā¸Ŗāš€ā¸Ŗā¸ĩā¸ĸā¸ā¸”ā¸šāšƒā¸šā¸Ģā¸™āš‰ā¸˛", "cant_get_number_of_comments": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸Ŗā¸ĩā¸ĸā¸ā¸”ā¸šā¸ˆā¸ŗā¸™ā¸§ā¸™ā¸„ā¸§ā¸˛ā¸Ąā¸„ā¸´ā¸”āš€ā¸Ģāš‡ā¸™āš„ā¸”āš‰", "cant_search_people": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸„āš‰ā¸™ā¸Ģā¸˛ā¸šā¸¸ā¸„ā¸„ā¸Ĩā¸„ā¸™āš„ā¸”āš‰", @@ -775,10 +854,12 @@ "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": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩ⏚ product key āš„ā¸”āš‰", "failed_to_stack_assets": "Failed to stack assets", "failed_to_unstack_assets": "Failed to un-stack assets", + "failed_to_update_notification_status": "ā¸­ā¸ąā¸žāš€ā¸”ā¸—ā¸Ēā¸–ā¸˛ā¸™ā¸°ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™āš„ā¸Ąāšˆā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "import_path_already_exists": "ā¸žā¸˛ā¸˜ā¸™ā¸ŗāš€ā¸‚āš‰ā¸˛ā¸™ā¸ĩāš‰ā¸Ąā¸ĩ⏭ā¸ĸā¸šāšˆāšā¸Ĩāš‰ā¸§", "incorrect_email_or_password": "⏭ā¸ĩāš€ā¸Ąā¸Ĩā¸Ģ⏪⏎⏭⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āš„ā¸Ąāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡", "paths_validation_failed": "ā¸ā¸˛ā¸Ŗā¸•ā¸Ŗā¸§ā¸ˆā¸Ē⏭⏚ {paths, plural, one {# path} other {# paths}} ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧", @@ -795,6 +876,7 @@ "unable_to_archive_unarchive": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸—ā¸ŗā¸Ŗā¸˛ā¸ĸ⏁⏞⏪ {archived, select, true {archive} other {unarchive}}", "unable_to_change_album_user_role": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸šā¸—ā¸šā¸˛ā¸—ā¸œā¸šāš‰āšƒā¸Šāš‰āšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāš„ā¸”āš‰", "unable_to_change_date": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆāš„ā¸”āš‰", + "unable_to_change_description": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸„ā¸ŗā¸­ā¸˜ā¸´ā¸šā¸˛ā¸ĸ", "unable_to_change_favorite": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸›ā¸Ĩ⏇ā¸Ēā¸ˇāšˆā¸­ā¸Ŗā¸˛ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”āš„ā¸”āš‰", "unable_to_change_location": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸•āšā¸˛āšā¸Ģā¸™āšˆā¸‡āš„ā¸”āš‰", "unable_to_change_password": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āš„ā¸”āš‰", @@ -838,6 +920,7 @@ "unable_to_remove_partner": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸šā¸„ā¸šāšˆā¸Ģā¸šāš„ā¸”āš‰", "unable_to_remove_reaction": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩ⏚ reaction āš„ā¸”āš‰", "unable_to_reset_password": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸•ā¸ąāš‰ā¸‡ā¸Ŗā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āšƒā¸Ģā¸Ąāšˆāš„ā¸”āš‰", + "unable_to_reset_pin_code": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ŗā¸ĩāš€ā¸‹āš‡ā¸•ā¸žā¸´ā¸™āš‚ā¸„āš‰ā¸”", "unable_to_resolve_duplicate": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āšā¸āš‰āš„ā¸‚ā¸‚ā¸­ā¸‡ā¸‹āš‰ā¸ŗāš„ā¸”āš‰", "unable_to_restore_assets": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸Ŗā¸ĩā¸ĸ⏁⏄⏎⏙ā¸Ēā¸ˇāšˆā¸­āš„ā¸”āš‰", "unable_to_restore_trash": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸Ŗā¸ĩā¸ĸā¸ā¸„ā¸ˇā¸™ā¸–ā¸ąā¸‡ā¸‚ā¸ĸā¸°āš„ā¸”āš‰", @@ -865,11 +948,15 @@ "unable_to_update_user": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸­ā¸ąā¸žāš€ā¸”ā¸—ā¸œā¸šāš‰āšƒā¸Šāš‰āš„ā¸”āš‰", "unable_to_upload_file": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āš„ā¸”āš‰" }, + "exif": "Exif", "exif_bottom_sheet_description": "āš€ā¸žā¸´āšˆā¸Ąā¸„ā¸ŗā¸­ā¸˜ā¸´ā¸šā¸˛ā¸ĸ", "exif_bottom_sheet_details": "⏪⏞ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸ⏔", "exif_bottom_sheet_location": "ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡", "exif_bottom_sheet_people": "⏄⏙", "exif_bottom_sheet_person_add_person": "āš€ā¸žā¸´āšˆā¸Ąā¸Šā¸ˇāšˆā¸­", + "exif_bottom_sheet_person_age_months": "⏭⏞ā¸ĸ⏏ {months} āš€ā¸”ā¸ˇā¸­ā¸™", + "exif_bottom_sheet_person_age_year_months": "⏭⏞ā¸ĸ⏏ 1 ⏛ā¸ĩ {months} āš€ā¸”ā¸ˇā¸­ā¸™", + "exif_bottom_sheet_person_age_years": "⏭⏞ā¸ĸ⏏ {years} ⏛ā¸ĩ", "exit_slideshow": "ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸ā¸˛ā¸Ŗā¸™ā¸ŗāš€ā¸Ē⏙⏭", "expand_all": "⏂ā¸ĸ⏞ā¸ĸā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "experimental_settings_new_asset_list_subtitle": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸žā¸ąā¸’ā¸™ā¸˛", @@ -886,9 +973,13 @@ "extension": "ā¸Ēāšˆā¸§ā¸™ā¸•āšˆā¸­ā¸‚ā¸ĸ⏞ā¸ĸ", "external": "⏠⏞ā¸ĸ⏙⏭⏁", "external_libraries": "⏠⏞ā¸ĸ⏙⏭⏁⏄ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸ž", - "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", + "external_network": "ā¸ā¸˛ā¸Ŗāš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸ ā¸˛ā¸ĸ⏙⏭⏁", + "external_network_sheet_info": "āš€ā¸Ąā¸ˇāšˆā¸­āš„ā¸Ąāšˆāš„ā¸”āš‰āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ Wi-Fi ⏗ā¸ĩāšˆāš€ā¸Ĩā¸ˇā¸­ā¸āš„ā¸§āš‰ āšā¸­ā¸žā¸ˆā¸°āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸œāšˆā¸˛ā¸™ URL ā¸”āš‰ā¸˛ā¸™ā¸Ĩāšˆā¸˛ā¸‡ā¸•ā¸˛ā¸Ąā¸Ĩā¸ŗā¸”ā¸ąā¸š", "face_unassigned": "āš„ā¸Ąāšˆā¸ā¸ŗā¸Ģā¸™ā¸”ā¸Ąā¸­ā¸šā¸Ģā¸Ąā¸˛ā¸ĸ", + "failed": "ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧", + "failed_to_authenticate": "⏁⏞⏪ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸•ā¸ąā¸§ā¸•ā¸™āš„ā¸Ąāšˆā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "failed_to_load_assets": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩā¸˛ā¸”āšƒā¸™ā¸ā¸˛ā¸Ŗāš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ˇāšˆā¸­", + "failed_to_load_folder": "āš‚ā¸Ģā¸Ĩā¸”āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒāš„ā¸Ąāšˆā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "favorite": "⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”", "favorite_or_unfavorite_photo": "āš‚ā¸›ā¸Ŗā¸”ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆāš‚ā¸›ā¸Ŗā¸”ā¸ ā¸˛ā¸ž", "favorites": "⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”", @@ -900,18 +991,26 @@ "file_name_or_extension": "ā¸™ā¸˛ā¸Ąā¸Ē⏁⏏ā¸Ĩā¸Ģā¸Ŗā¸ˇā¸­ā¸Šā¸ˇāšˆā¸­āš„ā¸Ÿā¸ĨāšŒ", "filename": "ā¸Šā¸ˇāšˆā¸­āš„ā¸Ÿā¸ĨāšŒ", "filetype": "ā¸Šā¸™ā¸´ā¸”āš„ā¸Ÿā¸ĨāšŒ", + "filter": "ā¸•ā¸ąā¸§ā¸ā¸Ŗā¸­ā¸‡", "filter_people": "ā¸ā¸Ŗā¸­ā¸‡ā¸œā¸šāš‰ā¸„ā¸™", + "filter_places": "⏁⏪⏭⏇ā¸Ē⏖⏞⏙⏗ā¸ĩāšˆ", "find_them_fast": "ā¸„āš‰ā¸™ā¸Ģā¸˛āš‚ā¸”ā¸ĸā¸Šā¸ˇāšˆā¸­ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸Ŗā¸§ā¸”āš€ā¸Ŗāš‡ā¸§", "fix_incorrect_match": "āšā¸āš‰āš„ā¸‚ā¸ā¸˛ā¸Ŗā¸ˆā¸ąā¸šā¸„ā¸šāšˆā¸—ā¸ĩāšˆāš„ā¸Ąāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡", + "folder": "āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒ", + "folder_not_found": "āš„ā¸Ąāšˆā¸žā¸šāš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒ", "folders": "āš‚ā¸Ÿā¸ĨāšŒāš€ā¸”ā¸­ā¸ŖāšŒ", "folders_feature_description": "ā¸ā¸˛ā¸Ŗāš€ā¸Ŗā¸ĩā¸ĸā¸ā¸”ā¸šā¸Ąā¸¸ā¸Ąā¸Ąā¸­ā¸‡āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ ā¸˛ā¸žā¸–āšˆā¸˛ā¸ĸāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­āšƒā¸™ā¸Ŗā¸°ā¸šā¸šāš„ā¸Ÿā¸ĨāšŒ", "forward": "āš„ā¸›ā¸‚āš‰ā¸˛ā¸‡ā¸Ģā¸™āš‰ā¸˛", + "gcast_enabled": "Google Cast", + "gcast_enabled_description": "⏟ā¸ĩāš€ā¸ˆā¸­ā¸ŖāšŒā¸™ā¸ĩāš‰ā¸•āš‰ā¸­ā¸‡āš‚ā¸Ģā¸Ĩā¸”ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪⏈⏞⏁ Google āš€ā¸žā¸ˇāšˆā¸­ā¸—ā¸ŗā¸‡ā¸˛ā¸™", "general": "ā¸—ā¸ąāšˆā¸§āš„ā¸›", "get_help": "ā¸‚ā¸­ā¸„ā¸§ā¸˛ā¸Ąā¸Šāšˆā¸§ā¸ĸāš€ā¸Ģā¸Ĩ⏎⏭", + "get_wifiname_error": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ŗā¸ąā¸šā¸Šā¸ˇāšˆā¸­ Wi-Fi ⏁⏪⏏⏓⏞ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸ā¸˛ā¸Ŗāšƒā¸Ģāš‰ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āšā¸­ā¸ž āšā¸Ĩ⏰ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸§āšˆā¸˛ Wi-Fi āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸­ā¸ĸā¸šāšˆ", "getting_started": "āš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "go_back": "⏁ā¸Ĩā¸ąā¸š", "go_to_folder": "āš„ā¸›ā¸—ā¸ĩāšˆāš‚ā¸Ÿā¸ĨāšŒāš€ā¸”ā¸­ā¸ŖāšŒ", "go_to_search": "⏁ā¸Ĩā¸ąā¸šāš„ā¸›ā¸ĸā¸ąā¸‡ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞", + "grant_permission": "āšƒā¸Ģāš‰ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•", "group_albums_by": "ā¸ˆā¸ąā¸”ā¸ā¸Ĩā¸¸āšˆā¸Ąā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸•ā¸˛ā¸Ą", "group_country": "ā¸ˆā¸ąā¸”āš€ā¸Ŗā¸ĩā¸ĸ⏇⏁ā¸Ĩā¸¸āšˆā¸Ąā¸•ā¸˛ā¸Ąā¸›ā¸Ŗā¸°āš€ā¸—ā¸¨", "group_no": "āš„ā¸Ąāšˆā¸ˆā¸ąā¸”ā¸ā¸Ĩā¸¸āšˆā¸Ą", @@ -921,6 +1020,11 @@ "haptic_feedback_switch": "āš€ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗā¸•ā¸­ā¸šā¸Ēā¸™ā¸­ā¸‡āšā¸šā¸šā¸Ēā¸ąā¸Ąā¸œā¸ąā¸Ē", "haptic_feedback_title": "ā¸ā¸˛ā¸Ŗā¸•ā¸­ā¸šā¸Ēā¸™ā¸­ā¸‡āšā¸šā¸šā¸Ēā¸ąā¸Ąā¸œā¸ąā¸Ē", "has_quota": "āš€ā¸Ģā¸Ĩā¸ˇā¸­ā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆ", + "header_settings_add_header_tip": "āš€ā¸žā¸´āšˆā¸Ą Header", + "header_settings_field_validator_msg": "ā¸„āšˆā¸˛ā¸•āš‰ā¸­ā¸‡āš„ā¸Ąāšˆā¸§āšˆā¸˛ā¸‡āš€ā¸›ā¸Ĩāšˆā¸˛", + "header_settings_header_name_input": "ā¸Šā¸ˇāšˆā¸­ Header", + "header_settings_header_value_input": "ā¸„āšˆā¸˛ Header", + "headers_settings_tile_title": "ā¸›ā¸Ŗā¸ąā¸šāšā¸•āšˆā¸‡ proxy headers", "hi_user": "ā¸Ēā¸§ā¸ąā¸Ē⏔ā¸ĩ⏄⏏⏓ {name} {email}", "hide_all_people": "ā¸‹āšˆā¸­ā¸™ā¸šā¸¸ā¸„ā¸„ā¸Ĩā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "hide_gallery": "ā¸‹āšˆā¸­ā¸™ā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸ž", @@ -929,22 +1033,28 @@ "hide_person": "ā¸‹āšˆā¸­ā¸™ā¸šā¸¸ā¸„ā¸„ā¸Ĩ", "hide_unnamed_people": "ā¸‹āšˆā¸­ā¸™ā¸šā¸¸ā¸„ā¸„ā¸Ĩ⏗ā¸ĩāšˆāš„ā¸Ąāšˆāš„ā¸”āš‰ā¸Ŗā¸°ā¸šā¸¸ā¸Šā¸ˇāšˆā¸­", "home_page_add_to_album_conflicts": "āš€ā¸žā¸´āšˆā¸Ą {added} ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāš€ā¸‚āš‰ā¸˛ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą {album}. {failed} ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪⏭ā¸ĸā¸šāšˆāšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸­ā¸ĸā¸šāšˆāšā¸Ĩāš‰ā¸§", - "home_page_add_to_album_err_local": " ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸žā¸´āšˆā¸Ąā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸šā¸™āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡āš€ā¸‚āš‰ā¸˛ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", + "home_page_add_to_album_err_local": "ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸žā¸´āšˆā¸Ąā¸Ēā¸ˇāšˆā¸­ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒāš€ā¸‚āš‰ā¸˛ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą ā¸‚āš‰ā¸˛ā¸Ą", "home_page_add_to_album_success": "āš€ā¸žā¸´āšˆā¸Ąā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪ {added} āš€ā¸‚āš‰ā¸˛ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą {album}", - "home_page_album_err_partner": "ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸žā¸´āšˆā¸Ąā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸‚ā¸­ā¸‡ā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗāš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", + "home_page_album_err_partner": "ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸žā¸´āšˆā¸Ąā¸Ēā¸ˇāšˆā¸­ā¸‚ā¸­ā¸‡ā¸„ā¸šāšˆā¸Ģā¸šāš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "home_page_archive_err_local": "ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗāš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", - "home_page_archive_err_partner": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸āš‡ā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸‚ā¸­ā¸‡ā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗāš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", + "home_page_archive_err_partner": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸āš‡ā¸šā¸Ēā¸ˇāšˆā¸­ā¸‚ā¸­ā¸‡ā¸„ā¸šāšˆā¸Ģā¸šāš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "home_page_building_timeline": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ timeline", - "home_page_delete_err_partner": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸‚ā¸­ā¸‡ā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗāš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", + "home_page_delete_err_partner": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩ⏚ā¸Ēā¸ˇāšˆā¸­ā¸‚ā¸­ā¸‡ā¸„ā¸šāšˆāš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "home_page_delete_remote_err_local": "ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸šā¸™āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ā¸­ā¸ĸā¸šāšˆāšƒā¸™ā¸Ĩ⏚⏈⏞⏁⏪ā¸ĩāš‚ā¸Ąā¸— ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "home_page_favorite_err_local": "ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸•ā¸ąāš‰ā¸‡ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸šā¸™āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡āš€ā¸›āš‡ā¸™ā¸Ŗā¸˛ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸” ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", - "home_page_favorite_err_partner": "ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸žā¸´āšˆā¸Ąā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸‚ā¸­ā¸‡ā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗāšƒā¸™ā¸Ŗā¸˛ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”āš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", + "home_page_favorite_err_partner": "ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸žā¸´āšˆā¸Ąā¸Ēā¸ˇāšˆā¸­ā¸‚ā¸­ā¸‡ā¸„ā¸šāšˆā¸Ģā¸šāšƒā¸™ā¸Ŗā¸˛ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”āš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "home_page_first_time_notice": "ā¸–āš‰ā¸˛ā¸„ā¸Ŗā¸ąāš‰ā¸‡ā¸™ā¸ĩāš‰āš€ā¸›āš‡ā¸™ā¸„ā¸Ŗā¸ąāš‰ā¸‡āšā¸Ŗā¸ā¸—ā¸ĩāšˆāšƒā¸Šāš‰āšā¸­ā¸›ā¸™ā¸ĩāš‰ ā¸ā¸Ŗā¸¸ā¸“ā¸˛āš€ā¸Ĩā¸ˇā¸­ā¸ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆā¸ˆā¸°ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ āš„ā¸—ā¸ĄāšŒāš„ā¸Ĩā¸™āšŒā¸ˆā¸°āš„ā¸”āš‰āš€ā¸žā¸´āšˆā¸Ąā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸­ā¸ĸā¸šāšˆāšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", + "home_page_locked_error_local": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ĸāš‰ā¸˛ā¸ĸā¸Ēā¸ˇāšˆā¸­ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒāš„ā¸›ā¸ĸā¸ąā¸‡āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ĩāš‡ā¸­ā¸„ ā¸‚āš‰ā¸˛ā¸Ą", + "home_page_locked_error_partner": "ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸žā¸´āšˆā¸Ąā¸Ēā¸ˇāšˆā¸­ā¸‚ā¸­ā¸‡ā¸„ā¸šāšˆā¸Ģā¸šāš„ā¸›ā¸ĸā¸ąā¸‡āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ĩāš‡ā¸­ā¸„āš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "home_page_share_err_local": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āšā¸Šā¸ŖāšŒā¸œāšˆā¸˛ā¸™ā¸Ĩā¸´ā¸‡ā¸„āšŒāš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "home_page_upload_err_limit": "ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩā¸”āš„ā¸”āš‰ā¸Ąā¸˛ā¸ā¸Ēā¸¸ā¸”ā¸„ā¸Ŗā¸ąāš‰ā¸‡ā¸Ĩ⏰ 30 ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "host": "āš‚ā¸Žā¸Ēā¸•āšŒ", "hour": "ā¸Šā¸ąāšˆā¸§āš‚ā¸Ąā¸‡", + "id": "āš„ā¸­ā¸”ā¸ĩ", + "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 {Video} other {Image}} ā¸–āšˆā¸˛ā¸ĸā¸ā¸ąā¸š {person1} ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ {date}", "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} ā¸–āšˆā¸˛ā¸ĸā¸ā¸ąā¸š {person1} āšā¸Ĩ⏰ {person2} ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ {date}", "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} ā¸–āšˆā¸˛ā¸ĸā¸ā¸ąā¸š {person1}, {person2},āšā¸Ĩ⏰ {person3} ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ {date}", @@ -954,6 +1064,7 @@ "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} ā¸–āšˆā¸˛ā¸ĸāšƒā¸™ {city}, {country} ā¸ā¸ąā¸š {person1} āšā¸Ĩ⏰ {person2} ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ {date}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} ā¸–āšˆā¸˛ā¸ĸāšƒā¸™ {city}, {country} ā¸ā¸ąā¸š {person1}, {person2},āšā¸Ĩ⏰ {person3} ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} ā¸–āšˆā¸˛ā¸ĸāšƒā¸™ {city}, {country} ā¸ā¸ąā¸š {person1}, {person2}, āšā¸Ĩ⏰ {additionalCount, number} āšƒā¸™ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ {date}", + "image_saved_successfully": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸–ā¸šā¸āš€ā¸‹ā¸Ÿ", "image_viewer_page_state_provider_download_started": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”āš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™", "image_viewer_page_state_provider_download_success": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "image_viewer_page_state_provider_share_error": "āšā¸Šā¸ŖāšŒā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔", @@ -974,8 +1085,16 @@ "night_at_midnight": "ā¸—ā¸¸ā¸āš€ā¸—ā¸ĩāšˆā¸ĸ⏇⏄⏎⏙", "night_at_twoam": "ā¸—ā¸¸ā¸ā¸§ā¸ąā¸™āš€ā¸§ā¸Ĩ⏞⏕ā¸ĩ 2" }, + "invalid_date": "ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆāš„ā¸Ąāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡", + "invalid_date_format": "ā¸Ŗā¸šā¸›āšā¸šā¸šā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆāš„ā¸Ąāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡", "invite_people": "āš€ā¸Šā¸´ā¸ā¸œā¸šāš‰ā¸„ā¸™", "invite_to_album": "āš€ā¸Šā¸´ā¸āš€ā¸‚āš‰ā¸˛ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", + "ios_debug_info_fetch_ran_at": "ā¸Ŗā¸ąā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš€ā¸Ąā¸ˇāšˆā¸­ {dateTime}", + "ios_debug_info_last_sync_at": "ā¸‹ā¸´ā¸‡ā¸„āšŒā¸Ĩāšˆā¸˛ā¸Ē⏏⏔ {dateTime}", + "ios_debug_info_no_processes_queued": "āš„ā¸Ąāšˆā¸Ąā¸ĩā¸„ā¸´ā¸§āšƒā¸™ā¸žā¸ˇāš‰ā¸™ā¸Ģā¸Ĩā¸ąā¸‡", + "ios_debug_info_no_sync_yet": "ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆā¸Ąā¸ĩā¸‡ā¸˛ā¸™ā¸‹ā¸´ā¸‡ā¸„āšŒā¸Ŗā¸ąā¸™āšƒā¸™ā¸žā¸ˇāš‰ā¸™ā¸Ģā¸Ĩā¸ąā¸‡", + "ios_debug_info_processes_queued": "{count} āš‚ā¸žā¸Ŗāš€ā¸‹ā¸Ēā¸Ŗā¸­ā¸„ā¸´ā¸§āšƒā¸™ā¸žā¸ˇāš‰ā¸™ā¸Ģā¸Ĩā¸ąā¸‡", + "ios_debug_info_processing_ran_at": "āš‚ā¸žā¸Ŗāš€ā¸‹ā¸Ēā¸Ŗā¸ąā¸™āš€ā¸Ąā¸ˇāšˆā¸­ {dateTime}", "items_count": "{count, plural, one {# ⏪⏞ā¸ĸ⏁⏞⏪} other {#⏪⏞ā¸ĸ⏁⏞⏪}}", "jobs": "⏇⏞⏙", "keep": "āš€ā¸āš‡ā¸š", @@ -984,6 +1103,9 @@ "kept_this_deleted_others": "āš€ā¸āš‡ā¸šāš€ā¸™ā¸ˇāš‰ā¸­ā¸Ģ⏞⏙ā¸ĩāš‰āšā¸Ĩ⏰ā¸Ĩ⏚ {count, plural, one {# Asset} other {# Asset}}", "keyboard_shortcuts": "ā¸›ā¸¸āšˆā¸Ąā¸žā¸´ā¸Ąā¸žāšŒā¸Ĩā¸ąā¸”", "language": "ā¸ ā¸˛ā¸Šā¸˛", + "language_no_results_subtitle": "ā¸ā¸Ŗā¸¸ā¸“ā¸˛ā¸›ā¸Ŗā¸ąā¸šāš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸„ā¸ŗā¸„āš‰ā¸™ā¸Ģ⏞", + "language_no_results_title": "āš„ā¸Ąāšˆā¸žā¸šā¸ ā¸˛ā¸Šā¸˛", + "language_search_hint": "ā¸„āš‰ā¸™ā¸Ģā¸˛ā¸ ā¸˛ā¸Šā¸˛...", "language_setting_description": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸ ā¸˛ā¸Šā¸˛ā¸—ā¸ĩāšˆā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗ", "last_seen": "āš€ā¸Ģāš‡ā¸™ā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "latest_version": "āš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™ā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", @@ -1009,14 +1131,21 @@ "list": "⏪⏞ā¸ĸ⏁⏞⏪", "loading": "⏁⏺ā¸Ĩā¸ąā¸‡āš‚ā¸Ģā¸Ĩ⏔", "loading_search_results_failed": "āš‚ā¸Ģā¸Ĩā¸”ā¸œā¸Ĩā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧", - "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", + "local_asset_cast_failed": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āšā¸„ā¸Ēā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆāš„ā¸Ąāšˆā¸–ā¸šā¸ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩā¸”āš„ā¸›ā¸ĸā¸ąā¸‡āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", + "local_network": "āš€ā¸„ā¸Ŗā¸ˇā¸­ā¸‚āšˆā¸˛ā¸ĸ⏪⏰ā¸ĸā¸°āšƒā¸ā¸Ĩāš‰", + "local_network_sheet_info": "āšā¸­ā¸žā¸ˆā¸°ā¸—ā¸ŗā¸ā¸˛ā¸Ŗāš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­āš„ā¸›ā¸ĸā¸ąā¸‡āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸œāšˆā¸˛ā¸™ URL ⏙ā¸ĩāš‰āš€ā¸Ąā¸ˇāšˆā¸­āš€ā¸Šā¸ˇāšˆā¸­ā¸•āšˆā¸­ā¸ā¸ąā¸š Wi-Fi ⏗ā¸ĩāšˆāš€ā¸Ĩā¸ˇā¸­ā¸āš„ā¸§āš‰", + "location_permission": "ā¸ā¸˛ā¸Ŗā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡", + "location_permission_content": "āš€ā¸žā¸ˇāšˆā¸­āšƒā¸Šāš‰ā¸Ÿā¸ĩāš€ā¸ˆā¸­ā¸ŖāšŒā¸ā¸˛ā¸Ŗā¸Ēā¸ąā¸šāš‚ā¸”ā¸ĸā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´ Immich ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡ā¸•āšˆā¸ŗāšā¸Ģā¸™āšˆā¸‡ā¸—ā¸ĩāšˆāšā¸Ąāšˆā¸™ā¸ĸā¸ŗāš€ā¸žā¸ˇāšˆā¸­ā¸­āšˆā¸˛ā¸™ā¸Šā¸ˇāšˆā¸­ Wi-Fi ⏗ā¸ĩāšˆāš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸­ā¸ĸā¸šāšˆ", "location_picker_choose_on_map": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸šā¸™āšā¸œā¸™ā¸—ā¸ĩāšˆ", "location_picker_latitude_error": "ā¸ā¸Ŗā¸¸ā¸“ā¸˛āš€ā¸žā¸´āšˆā¸Ąā¸Ĩā¸°ā¸•ā¸´ā¸ˆā¸šā¸•ā¸—ā¸ĩāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡", "location_picker_latitude_hint": "āš€ā¸žā¸´āšˆā¸Ąā¸Ĩā¸°ā¸•ā¸´ā¸ˆā¸šā¸•ā¸•ā¸Ŗā¸‡ā¸™ā¸ĩāš‰", "location_picker_longitude_error": "ā¸ā¸Ŗā¸¸ā¸“ā¸˛āš€ā¸žā¸´āšˆā¸Ąā¸Ĩā¸­ā¸‡ā¸ˆā¸´ā¸ˆā¸šā¸•ā¸—ā¸ĩāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡", "location_picker_longitude_hint": "āš€ā¸žā¸´āšˆā¸Ąā¸Ĩā¸­ā¸‡ā¸ˆā¸´ā¸ˆā¸šā¸•ā¸•ā¸Ŗā¸‡ā¸™ā¸ĩāš‰", + "lock": "ā¸Ĩāš‡ā¸­ā¸„", + "locked_folder": "āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ĩāš‡ā¸­ā¸„", "log_out": "⏭⏭⏁⏈⏞⏁⏪⏰⏚⏚", "log_out_all_devices": "āšƒā¸Ģāš‰ā¸—ā¸¸ā¸ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸Ŗā¸°ā¸šā¸šā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", + "logged_in_as": "{user} ⏁⏺ā¸Ĩā¸ąā¸‡ā¸Ĩāš‡ā¸­ā¸„ā¸­ā¸´ā¸™", "logged_out_all_devices": "ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸Ŗā¸°ā¸šā¸šā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āšā¸Ĩāš‰ā¸§", "logged_out_device": "ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸Ŗā¸°ā¸šā¸šāšā¸Ĩāš‰ā¸§", "login": "āš€ā¸‚āš‰ā¸˛ā¸Ēā¸šāšˆā¸Ŗā¸°ā¸šā¸š", @@ -1079,7 +1208,7 @@ "map_settings_date_range_option_years": "{years} ⏛ā¸ĩā¸œāšˆā¸˛ā¸™ā¸Ąā¸˛", "map_settings_dialog_title": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āšā¸œā¸™ā¸—ā¸ĩāšˆ", "map_settings_include_show_archived": "ā¸Ŗā¸§ā¸Ąāš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ", - "map_settings_include_show_partners": "ā¸Ŗā¸˛ā¸Ąā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗ", + "map_settings_include_show_partners": "ā¸Ŗā¸§ā¸Ąā¸„ā¸šāšˆā¸Ģā¸š", "map_settings_only_show_favorites": "āšā¸Ē⏔⏇⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”āš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™", "map_settings_theme_settings": "⏘ā¸ĩā¸Ąāšā¸œā¸™ā¸—ā¸ĩāšˆ", "map_zoom_to_see_photos": "ā¸‹ā¸šā¸Ąā¸­ā¸­ā¸āš€ā¸žā¸ˇāšˆā¸­ā¸”ā¸šā¸Ŗā¸šā¸›", @@ -1106,12 +1235,17 @@ "model": "āš‚ā¸Ąāš€ā¸”ā¸Ĩ", "month": "āš€ā¸”ā¸ˇā¸­ā¸™", "more": "āš€ā¸žā¸´āšˆā¸Ąāš€ā¸•ā¸´ā¸Ą", + "move": "ā¸ĸāš‰ā¸˛ā¸ĸ", + "move_off_locked_folder": "ā¸ĸāš‰ā¸˛ā¸ĸā¸­ā¸­ā¸ā¸ˆā¸˛ā¸āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ĩāš‡ā¸­ā¸„", + "move_to_locked_folder": "ā¸ĸāš‰ā¸˛ā¸ĸāš„ā¸›āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ĩāš‡ā¸­ā¸„", "moved_to_trash": "ā¸—ā¸´āš‰ā¸‡ā¸Ĩā¸‡ā¸–ā¸ąā¸‡ā¸‚ā¸ĸā¸°āšā¸Ĩāš‰ā¸§", "multiselect_grid_edit_date_time_err_read_only": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āšā¸āš‰āš„ā¸‚ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšā¸šā¸šā¸­āšˆā¸˛ā¸™ā¸­ā¸ĸāšˆā¸˛ā¸‡āš€ā¸”ā¸ĩā¸ĸ⏧ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "multiselect_grid_edit_gps_err_read_only": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āšā¸āš‰ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡ā¸‚ā¸­ā¸‡ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšā¸šā¸šā¸­āšˆā¸˛ā¸™ā¸­ā¸ĸāšˆā¸˛ā¸‡āš€ā¸”ā¸ĩā¸ĸ⏧ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "my_albums": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸‚ā¸­ā¸‡ā¸‰ā¸ąā¸™", "name": "ā¸Šā¸ˇāšˆā¸­", "name_or_nickname": "ā¸Šā¸ˇāšˆā¸­ā¸Ģā¸Ŗā¸ˇā¸­ā¸Šā¸ˇāšˆā¸­āš€ā¸Ĩāšˆā¸™", + "networking_settings": "ā¸ā¸˛ā¸Ŗāš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­", + "networking_subtitle": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸›ā¸Ĩ⏞ā¸ĸā¸—ā¸˛ā¸‡āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", "never": "āš„ā¸Ąāšˆāš€ā¸„ā¸ĸ", "new_album": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšƒā¸Ģā¸Ąāšˆ", "new_api_key": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ API ⏄ā¸ĩā¸ĸāšŒāšƒā¸Ģā¸Ąāšˆ", @@ -1155,7 +1289,7 @@ "ok": "⏕⏁ā¸Ĩ⏇", "oldest_first": "āš€ā¸Ŗā¸ĩā¸ĸā¸‡āš€ā¸āšˆā¸˛ā¸Ēā¸¸ā¸”ā¸āšˆā¸­ā¸™", "onboarding": "ā¸ā¸˛ā¸Ŗāš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", - "onboarding_privacy_description": "⏄⏏⏓ā¸Ĩā¸ąā¸ā¸Šā¸“ā¸° (āš„ā¸Ąāšˆā¸ˆā¸ŗāš€ā¸›āš‡ā¸™) ā¸•āšˆā¸­āš„ā¸›ā¸™ā¸ĩāš‰ā¸•āš‰ā¸­ā¸‡ā¸­ā¸˛ā¸¨ā¸ąā¸ĸ⏚⏪⏴⏁⏞⏪⏠⏞ā¸ĸ⏙⏭⏁ āšā¸Ĩ⏰ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āš„ā¸”āš‰ā¸•ā¸Ĩā¸­ā¸”āš€ā¸§ā¸Ĩā¸˛āšƒā¸™ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸”ā¸šāšā¸Ĩ⏪⏰⏚⏚", + "onboarding_privacy_description": "⏟ā¸ĩāš€ā¸ˆā¸­ā¸ŖāšŒ (ā¸•ā¸ąā¸§āš€ā¸Ĩ⏎⏭⏁) ā¸•āšˆā¸­āš„ā¸›ā¸™ā¸ĩāš‰ā¸•āš‰ā¸­ā¸‡ā¸­ā¸˛ā¸¨ā¸ąā¸ĸ⏚⏪⏴⏁⏞⏪⏠⏞ā¸ĸ⏙⏭⏁ āšā¸Ĩ⏰ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āš„ā¸”āš‰ā¸•ā¸Ĩā¸­ā¸”āš€ā¸§ā¸Ĩā¸˛āšƒā¸™ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗ", "onboarding_theme_description": "āš€ā¸Ĩ⏎⏭⏁⏘ā¸ĩā¸Ąā¸Ēā¸ĩ ⏄⏏⏓ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸›ā¸Ĩā¸‡āš„ā¸”āš‰āšƒā¸™ā¸ ā¸˛ā¸ĸā¸Ģā¸Ĩā¸ąā¸‡āšƒā¸™ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", "onboarding_welcome_user": "ā¸ĸ⏴⏙⏔ā¸ĩā¸•āš‰ā¸­ā¸™ā¸Ŗā¸ąā¸šā¸„ā¸¸ā¸“ {user}", "online": "ā¸­ā¸­ā¸™āš„ā¸Ĩā¸™āšŒ", @@ -1172,20 +1306,20 @@ "other_variables": "ā¸•ā¸ąā¸§āšā¸›ā¸Ŗā¸­ā¸ˇāšˆā¸™", "owned": "āš€ā¸›āš‡ā¸™āš€ā¸ˆāš‰ā¸˛ā¸‚ā¸­ā¸‡", "owner": "āš€ā¸ˆāš‰ā¸˛ā¸‚ā¸­ā¸‡", - "partner": "ā¸žā¸˛ā¸ŖāšŒā¸—āš€ā¸™ā¸­ā¸ŖāšŒ", + "partner": "ā¸„ā¸šāšˆā¸Ģā¸š", "partner_can_access": "{partner} ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļ⏇", "partner_can_access_assets": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸ĸā¸āš€ā¸§āš‰ā¸™ā¸—ā¸ĩāšˆā¸­ā¸ĸā¸šāšˆāšƒā¸™āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗāšā¸Ĩā¸°ā¸–ā¸šā¸ā¸Ĩā¸šā¸—ā¸´āš‰ā¸‡", "partner_can_access_location": "ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡ā¸—ā¸ĩāšˆā¸Ŗā¸šā¸›ā¸–ā¸šā¸ā¸–āšˆā¸˛ā¸ĸ", "partner_list_user_photos": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸‚ā¸­ā¸‡ {user}", "partner_list_view_all": "ā¸”ā¸šā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", - "partner_page_empty_message": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆā¸–ā¸šā¸āšā¸Šā¸ŖāšŒā¸ā¸ąā¸šā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗ", + "partner_page_empty_message": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆā¸–ā¸šā¸āšā¸Šā¸ŖāšŒā¸ā¸ąā¸šā¸„ā¸šāšˆā¸Ģā¸š", "partner_page_no_more_users": "āš„ā¸Ąāšˆā¸Ąā¸ĩā¸œā¸šāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āšƒā¸Ģāš‰āš€ā¸žā¸´āšˆā¸Ą", - "partner_page_partner_add_failed": "ā¸ā¸˛ā¸Ŗāš€ā¸žā¸´āšˆā¸Ąā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧", - "partner_page_select_partner": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗ", + "partner_page_partner_add_failed": "ā¸ā¸˛ā¸Ŗāš€ā¸žā¸´āšˆā¸Ąā¸„ā¸šāšˆā¸Ģā¸šā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧", + "partner_page_select_partner": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸„ā¸šāšˆā¸Ģā¸š", "partner_page_shared_to_title": "āšā¸Šā¸ŖāšŒā¸ā¸ąā¸š", "partner_page_stop_sharing_content": "{partner} ā¸ˆā¸°āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", - "partner_sharing": "āšā¸Šā¸ŖāšŒā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸žā¸˛ā¸ŖāšŒā¸—āš€ā¸™ā¸­ā¸ŖāšŒ", - "partners": "ā¸žā¸˛ā¸ŖāšŒā¸—āš€ā¸™ā¸­ā¸ŖāšŒ", + "partner_sharing": "āšā¸Šā¸ŖāšŒā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸„ā¸šāšˆā¸Ģā¸š", + "partners": "ā¸„ā¸šāšˆā¸Ģā¸š", "password": "⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™", "password_does_not_match": "⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āš„ā¸Ąāšˆā¸•ā¸Ŗā¸‡ā¸ā¸ąā¸™", "password_required": "ā¸ˆā¸ŗāš€ā¸›āš‡ā¸™ā¸•āš‰ā¸­ā¸‡ā¸Ąā¸ĩ⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™", @@ -1276,7 +1410,7 @@ "purchase_lifetime_description": "ā¸‹ā¸ˇāš‰ā¸­ā¸•ā¸Ĩā¸­ā¸”ā¸Šā¸ĩā¸ž", "purchase_option_title": "ā¸•ā¸ąā¸§āš€ā¸Ĩā¸ˇā¸­ā¸ā¸ā¸˛ā¸Ŗā¸‹ā¸ˇāš‰ā¸­", "purchase_panel_info_1": "⏗⏞⏇⏗ā¸ĩā¸Ą Immich ā¸•āš‰ā¸­ā¸‡āšƒā¸Šāš‰āš€ā¸§ā¸Ĩā¸˛āšā¸Ĩā¸°ā¸„ā¸§ā¸˛ā¸Ąā¸žā¸ĸ⏞ā¸ĸā¸˛ā¸Ąā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸Ąā¸˛ā¸āšƒā¸™ā¸ā¸˛ā¸Ŗā¸žā¸ąā¸’ā¸™ā¸˛ā¸Ŗā¸°ā¸šā¸šā¸™ā¸ĩāš‰ā¸‚ā¸ļāš‰ā¸™ā¸Ąā¸˛ āšā¸Ĩā¸°āš€ā¸Ŗā¸˛ā¸Ąā¸ĩ⏧⏴⏍⏧⏁⏪⏗ā¸ĩāšˆā¸—ā¸ŗā¸‡ā¸˛ā¸™āš€ā¸•āš‡ā¸Ąāš€ā¸§ā¸Ĩā¸˛āš€ā¸žā¸ˇāšˆā¸­ā¸žā¸ąā¸’ā¸™ā¸˛āšƒā¸Ģāš‰ā¸”ā¸ĩ⏗ā¸ĩāšˆā¸Ēā¸¸ā¸”āš€ā¸—āšˆā¸˛ā¸—ā¸ĩāšˆā¸ˆā¸°ā¸—ā¸ŗāš„ā¸”āš‰ ā¸ ā¸˛ā¸Ŗā¸ā¸´ā¸ˆā¸‚ā¸­ā¸‡āš€ā¸Ŗā¸˛ā¸„ā¸ˇā¸­ā¸ā¸˛ā¸Ŗā¸—ā¸ŗāšƒā¸Ģāš‰ā¸‹ā¸­ā¸Ÿā¸•āšŒāšā¸§ā¸ŖāšŒāš‚ā¸­āš€ā¸žāšˆā¸™ā¸‹ā¸­ā¸ŖāšŒā¸Ēāšā¸Ĩā¸°āšā¸™ā¸§ā¸—ā¸˛ā¸‡ā¸›ā¸ā¸´ā¸šā¸ąā¸•ā¸´ā¸—ā¸˛ā¸‡ā¸˜ā¸¸ā¸Ŗā¸ā¸´ā¸ˆā¸—ā¸ĩāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡ā¸•ā¸˛ā¸Ąā¸ˆā¸Ŗā¸´ā¸ĸā¸˜ā¸Ŗā¸Ŗā¸Ąā¸ā¸Ĩ⏞ā¸ĸāš€ā¸›āš‡ā¸™āšā¸Ģā¸Ĩāšˆā¸‡ā¸Ŗā¸˛ā¸ĸāš„ā¸”āš‰ā¸—ā¸ĩāšˆā¸ĸā¸ąāšˆā¸‡ā¸ĸ⏎⏙ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸™ā¸ąā¸ā¸žā¸ąā¸’ā¸™ā¸˛ āšā¸Ĩ⏰ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸Ŗā¸°ā¸šā¸šā¸™ā¸´āš€ā¸§ā¸¨ā¸—ā¸ĩāšˆāš€ā¸„ā¸˛ā¸Ŗā¸žā¸„ā¸§ā¸˛ā¸Ąāš€ā¸›āš‡ā¸™ā¸Ēāšˆā¸§ā¸™ā¸•ā¸ąā¸§ā¸žā¸Ŗāš‰ā¸­ā¸Ąā¸—ā¸˛ā¸‡āš€ā¸Ĩā¸ˇā¸­ā¸ā¸­ā¸ˇāšˆā¸™ā¸—ā¸ĩāšˆāš€ā¸›āš‡ā¸™ā¸Ŗā¸šā¸›ā¸˜ā¸Ŗā¸Ŗā¸Ąāšā¸—ā¸™ā¸šā¸Ŗā¸´ā¸ā¸˛ā¸Ŗā¸„ā¸Ĩā¸˛ā¸§ā¸”āšŒā¸—ā¸ĩāšˆāš€ā¸­ā¸˛ā¸Ŗā¸ąā¸”āš€ā¸­ā¸˛āš€ā¸›ā¸Ŗā¸ĩā¸ĸ⏚", - "purchase_panel_info_2": "āš€ā¸™ā¸ˇāšˆā¸­ā¸‡ā¸ˆā¸˛ā¸āš€ā¸Ŗā¸˛āšƒā¸Ģāš‰ā¸„ā¸ŗā¸Ąā¸ąāšˆā¸™ā¸§āšˆā¸˛ ā¸ˆā¸°āš„ā¸Ąāšˆāš€ā¸žā¸´āšˆā¸Ąā¸Ŗā¸°ā¸šā¸šā¸Šā¸ŗā¸Ŗā¸°āš€ā¸‡ā¸´ā¸™āšƒā¸™ā¸Ŗā¸°ā¸šā¸šā¸‚ā¸­ā¸‡āš€ā¸Ŗā¸˛ ā¸”ā¸ąā¸‡ā¸™ā¸ąāš‰ā¸™ā¸ā¸˛ā¸Ŗā¸‹ā¸ˇāš‰ā¸­ā¸„ā¸Ŗā¸ąāš‰ā¸‡ā¸™ā¸ĩāš‰ā¸ˆā¸°āš„ā¸Ąāšˆā¸—ā¸ŗāšƒā¸Ģāš‰ā¸„ā¸¸ā¸“āš„ā¸”āš‰ā¸Ŗā¸ąā¸šā¸Ÿā¸ĩāš€ā¸ˆā¸­ā¸ŖāšŒāš€ā¸žā¸´āšˆā¸Ąāš€ā¸•ā¸´ā¸Ąāšƒā¸™ Immich āš€ā¸›āš‡ā¸™ā¸žā¸´āš€ā¸¨ā¸Š āš€ā¸Ŗā¸˛ā¸­ā¸˛ā¸¨ā¸ąā¸ĸā¸œā¸šāš‰ā¸„ā¸™āšā¸šā¸šā¸—āšˆā¸˛ā¸™āšƒā¸™ā¸ā¸˛ā¸Ŗā¸Ēā¸™ā¸ąā¸šā¸Ēā¸™ā¸¸ā¸™ā¸ā¸˛ā¸Ŗā¸žā¸ąā¸’ā¸™ā¸˛ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸•āšˆā¸­āš€ā¸™ā¸ˇāšˆā¸­ā¸‡ā¸‚ā¸­ā¸‡ Immich", + "purchase_panel_info_2": "āš€ā¸™ā¸ˇāšˆā¸­ā¸‡ā¸ˆā¸˛ā¸āš€ā¸Ŗā¸˛āšƒā¸Ģāš‰ā¸„ā¸ŗā¸Ąā¸ąāšˆā¸™ā¸§āšˆā¸˛ ā¸ˆā¸°āš„ā¸Ąāšˆāš€ā¸žā¸´āšˆā¸Ąā¸Ŗā¸°ā¸šā¸šā¸Šā¸ŗā¸Ŗā¸°āš€ā¸‡ā¸´ā¸™āšƒā¸™ā¸Ŗā¸°ā¸šā¸šā¸‚ā¸­ā¸‡āš€ā¸Ŗā¸˛ ā¸”ā¸ąā¸‡ā¸™ā¸ąāš‰ā¸™ā¸ā¸˛ā¸Ŗā¸‹ā¸ˇāš‰ā¸­ā¸„ā¸Ŗā¸ąāš‰ā¸‡ā¸™ā¸ĩāš‰ā¸ˆā¸°āš„ā¸Ąāšˆā¸—ā¸ŗāšƒā¸Ģāš‰ā¸„ā¸¸ā¸“āš„ā¸”āš‰ā¸Ŗā¸ąā¸šā¸Ÿā¸ĩāš€ā¸ˆā¸­ā¸ŖāšŒāš€ā¸žā¸´āšˆā¸Ąāš€ā¸•ā¸´ā¸Ąāšƒā¸™ Immich āš€ā¸›āš‡ā¸™ā¸žā¸´āš€ā¸¨ā¸Š āš€ā¸Ŗā¸˛ā¸­ā¸˛ā¸¨ā¸ąā¸ĸā¸œā¸šāš‰ā¸„ā¸™āšā¸šā¸šā¸„ā¸¸ā¸“āšƒā¸™ā¸ā¸˛ā¸Ŗā¸Ēā¸™ā¸ąā¸šā¸Ēā¸™ā¸¸ā¸™ā¸ā¸˛ā¸Ŗā¸žā¸ąā¸’ā¸™ā¸˛ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸•āšˆā¸­āš€ā¸™ā¸ˇāšˆā¸­ā¸‡ā¸‚ā¸­ā¸‡ Immich", "purchase_panel_title": "ā¸Ēā¸™ā¸ąā¸šā¸Ēā¸™ā¸¸ā¸™āš‚ā¸„ā¸Ŗā¸‡ā¸ā¸˛ā¸Ŗā¸™ā¸ĩāš‰", "purchase_per_server": "ā¸•āšˆā¸­āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", "purchase_per_user": "ā¸•āšˆā¸­ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", @@ -1421,6 +1555,7 @@ "select_keep_all": "āš€ā¸Ĩā¸ˇā¸­ā¸āš€ā¸āš‡ā¸šā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "select_library_owner": "āš€ā¸Ĩā¸ˇā¸­ā¸āš€ā¸ˆāš‰ā¸˛ā¸‚ā¸­ā¸‡ā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸ž", "select_new_face": "āš€ā¸Ĩā¸ˇā¸­ā¸āšƒā¸šā¸Ģā¸™āš‰ā¸˛āšƒā¸Ģā¸Ąāšˆ", + "select_person_to_tag": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸šā¸¸ā¸„ā¸„ā¸Ĩ", "select_photos": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", "select_trash_all": "āš€ā¸Ĩā¸ˇā¸­ā¸āšƒā¸™ā¸–ā¸ąā¸‡ā¸‚ā¸ĸā¸°ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "select_user_for_sharing_page_err_album": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧", @@ -1428,12 +1563,14 @@ "selected_count": "{count, plural, other {# āš€ā¸Ĩā¸ˇā¸­ā¸āšā¸Ĩāš‰ā¸§}}", "send_message": "ā¸Ēāšˆā¸‡ā¸‚āš‰ā¸­ā¸„ā¸§ā¸˛ā¸Ą", "send_welcome_email": "ā¸Ēāšˆā¸‡ā¸­ā¸ĩāš€ā¸Ąā¸Ĩā¸•āš‰ā¸­ā¸™ā¸Ŗā¸ąā¸š", + "server_endpoint": "⏛ā¸Ĩ⏞ā¸ĸā¸—ā¸˛ā¸‡āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", "server_info_box_app_version": "āš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™āšā¸­ā¸ž", "server_info_box_server_url": "URL āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", "server_offline": "Server ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒ", "server_online": "Server ā¸­ā¸­ā¸™āš„ā¸Ĩā¸™āšŒ", + "server_privacy": "ā¸„ā¸§ā¸˛ā¸Ąāš€ā¸›āš‡ā¸™ā¸Ēāšˆā¸§ā¸™ā¸•ā¸ąā¸§āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", "server_stats": "ā¸Ēā¸–ā¸´ā¸•ā¸´āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", - "server_version": "āš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™ā¸‚ā¸­ā¸‡ Server", + "server_version": "āš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™ā¸‚ā¸­ā¸‡āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", "set": "ā¸•ā¸ąāš‰ā¸‡", "set_as_album_cover": "ā¸•ā¸ąāš‰ā¸‡āš€ā¸›āš‡ā¸™ā¸ ā¸˛ā¸žā¸›ā¸ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "set_as_featured_photo": "ā¸•ā¸ąāš‰ā¸‡āš€ā¸›āš‡ā¸™ā¸Ŗā¸šā¸›ā¸Ēā¸ŗā¸„ā¸ąā¸", @@ -1518,7 +1655,7 @@ "sharing_page_empty_list": "⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗā¸§āšˆā¸˛ā¸‡āš€ā¸›ā¸Ĩāšˆā¸˛", "sharing_sidebar_description": "āšā¸Ē⏔⏇ā¸Ĩā¸´ā¸‡ā¸āšŒā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒāšƒā¸™āšā¸–ā¸šā¸”āš‰ā¸˛ā¸™ā¸‚āš‰ā¸˛ā¸‡", "sharing_silver_appbar_create_shared_album": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒāšƒā¸Ģā¸Ąāšˆ", - "sharing_silver_appbar_share_partner": "āšā¸Šā¸ŖāšŒā¸ā¸ąā¸šā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗ", + "sharing_silver_appbar_share_partner": "āšā¸Šā¸ŖāšŒā¸ā¸ąā¸šā¸„ā¸šāšˆā¸Ģā¸š", "shift_to_permanent_delete": "⏁⏔ ⇧ to ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸Ĩ⏚ā¸Ēā¸ˇāšˆā¸­ā¸–ā¸˛ā¸§ā¸Ŗ", "show_album_options": "āšā¸Ēā¸”ā¸‡ā¸•ā¸ąā¸§āš€ā¸Ĩā¸ˇā¸­ā¸ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "show_albums": "āšā¸Ēā¸”ā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", @@ -1570,7 +1707,7 @@ "status": "ā¸Ē⏖⏞⏙⏰", "stop_motion_photo": "ā¸ ā¸˛ā¸žā¸§ā¸ąā¸•ā¸–ā¸¸āš€ā¸„ā¸Ĩā¸ˇāšˆā¸­ā¸™āš„ā¸Ģ⏧", "stop_photo_sharing": "ā¸Ģā¸ĸā¸¸ā¸”āšā¸Šā¸ŖāšŒā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž?", - "stop_photo_sharing_description": "{partner}ā¸ˆā¸°āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡ā¸Ŗā¸šā¸›ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš„ā¸”āš‰ā¸­ā¸ĩ⏁", + "stop_photo_sharing_description": "{partner} ā¸ˆā¸°āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡ā¸Ŗā¸šā¸›ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš„ā¸”āš‰ā¸­ā¸ĩ⏁", "stop_sharing_photos_with_user": "ā¸Ģā¸ĸā¸¸ā¸”ā¸ā¸˛ā¸Ŗāšā¸Šā¸ŖāšŒā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸ā¸ąā¸šā¸œā¸šāš‰āšƒā¸Šāš‰ā¸™ā¸ĩāš‰", "storage": "ā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸ˆā¸ąā¸”āš€ā¸āš‡ā¸š", "storage_label": "āš€ā¸™ā¸ˇāš‰ā¸­ā¸—ā¸ĩāšˆā¸ˆā¸ąā¸”āš€ā¸āš‡ā¸š", @@ -1644,6 +1781,7 @@ "unselect_all": "ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗāš€ā¸Ĩā¸ˇā¸­ā¸ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "unstack": "ā¸Ģā¸ĸā¸¸ā¸”ā¸‹āš‰ā¸­ā¸™", "up_next": "ā¸•āšˆā¸­āš„ā¸›", + "updated_at": "ā¸­ā¸ąā¸žāš€ā¸”ā¸—", "updated_password": "⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸Ĩāš‰ā¸§", "upload": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔", "upload_concurrency": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”ā¸žā¸Ŗāš‰ā¸­ā¸Ąā¸ā¸ąā¸™", @@ -1653,7 +1791,9 @@ "upload_status_errors": "ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔", "upload_status_uploaded": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āšā¸Ĩāš‰ā¸§", "upload_success": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ, ⏪ā¸ĩāš€ā¸Ÿā¸Ŗā¸Šā¸Ģā¸™āš‰ā¸˛ā¸™ā¸ĩāš‰āšƒā¸Ģā¸Ąāšˆā¸„ā¸¸ā¸“ā¸ˆā¸°āš€ā¸Ģāš‡ā¸™ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆāš€ā¸žā¸´āšˆā¸Ąā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", + "uploading": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩ⏔", "usage": "ā¸ā¸˛ā¸Ŗāšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", + "use_biometric": "āšƒā¸Šāš‰ā¸ā¸˛ā¸Ŗā¸žā¸´ā¸Ēā¸šā¸ˆā¸™āšŒā¸­ā¸ąā¸•ā¸Ĩā¸ąā¸ā¸Šā¸“āšŒ", "use_custom_date_range": "āšƒā¸Šāš‰ā¸ā¸˛ā¸Ŗā¸›ā¸Ŗā¸ąā¸šāšā¸•āšˆā¸‡ā¸Šāšˆā¸§ā¸‡āš€ā¸§ā¸Ĩ⏞", "user": "ā¸œā¸šāš‰āšƒā¸Šāš‰", "user_id": "āš„ā¸­ā¸”ā¸ĩā¸œā¸šāš‰āšƒā¸Šāš‰", @@ -1686,6 +1826,7 @@ "view_links": "ā¸”ā¸šā¸Ĩā¸´ā¸‡ā¸āšŒ", "view_next_asset": "ā¸”ā¸šā¸Ēā¸ˇāšˆā¸­ā¸–ā¸ąā¸”āš„ā¸›", "view_previous_asset": "ā¸”ā¸šā¸Ēā¸ˇāšˆā¸­ā¸āšˆā¸­ā¸™ā¸Ģā¸™āš‰ā¸˛", + "view_qr_code": "ā¸”ā¸šā¸„ā¸´ā¸§ā¸­ā¸˛ā¸ŖāšŒāš‚ā¸„āš‰ā¸”", "viewer_remove_from_stack": "āš€ā¸­ā¸˛ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸—ā¸ĩāšˆā¸‹āš‰ā¸­ā¸™", "viewer_stack_use_as_main_asset": "āšƒā¸Šāš‰āš€ā¸›āš‡ā¸™ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪ā¸Ģā¸Ĩā¸ąā¸", "viewer_unstack": "ā¸Ģā¸ĸā¸¸ā¸”ā¸‹āš‰ā¸­ā¸™", @@ -1695,11 +1836,11 @@ "week": "ā¸Ēā¸ąā¸›ā¸”ā¸˛ā¸ĢāšŒ", "welcome": "ā¸ĸ⏴⏙⏔ā¸ĩā¸•āš‰ā¸­ā¸™ā¸Ŗā¸ąā¸š", "welcome_to_immich": "ā¸ĸ⏴⏙⏔ā¸ĩā¸•āš‰ā¸­ā¸™ā¸Ŗā¸ąā¸šā¸Ēā¸šāšˆ immich", - "wifi_name": "WiFi Name", + "wifi_name": "ā¸Šā¸ˇāšˆā¸­ Wi-Fi", "year": "⏛ā¸ĩ", "years_ago": "{years, plural, one {# ⏛ā¸ĩ} other {# ⏛ā¸ĩ}} ⏗ā¸ĩāšˆāšā¸Ĩāš‰ā¸§", "yes": "āšƒā¸Šāšˆ", "you_dont_have_any_shared_links": "ā¸„ā¸¸ā¸“āš„ā¸Ąāšˆāš„ā¸”āš‰ā¸Ąā¸ĩā¸Ĩā¸´ā¸‡ā¸āšŒā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒ", - "your_wifi_name": "Your WiFi name", + "your_wifi_name": "ā¸Šā¸ˇāšˆā¸­ Wi-Fi", "zoom_image": "ā¸‹ā¸šā¸Ąā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž" } diff --git a/i18n/tr.json b/i18n/tr.json index ddb99ca019..b26f70acc2 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -34,6 +34,7 @@ "added_to_favorites_count": "{count, number} fotoğraf favorilere eklendi", "admin": { "add_exclusion_pattern_description": "Hariç tutma desenleri ekleyin. *, ** ve ? kullanÄąlarak Globbing (temsili yer doldurucu karakter) desteklenir. Farzedelim \"Raw\" adlÄą bir dizininiz var, içinde ki tÃŧm dosyalarÄą yoksaymak için \"**/Raw/**\" şeklinde yazabilirsiniz. \".tif\" ile biten tÃŧm dosyalarÄą yoksaymak için \"**/*.tif\" yazabilirsiniz. Mutlak yolu yoksaymak için \"/yoksayÄąlacak/olan/yol/**\" şeklinde yazabilirsiniz.", + "admin_user": "YÃļnetici kullanÄącÄąsÄą", "asset_offline_description": "Bu harici kÃŧtÃŧphane varlığı artÄąk diskte bulunmuyor ve çÃļp kutusuna taÅŸÄąndÄą. Dosya kÃŧtÃŧphane içinde taÅŸÄąndÄąysa, yeni karÅŸÄąlÄąk gelen varlÄąk için zaman çizelgenizi kontrol edin. Bu varlığı geri yÃŧklemek için lÃŧtfen aşağıdaki dosya yolunun Immich tarafÄąndan erişilebilir olduğundan emin olun ve kÃŧtÃŧphaneyi tarayÄąn.", "authentication_settings": "Yetkilendirme AyarlarÄą", "authentication_settings_description": "Şifre, OAuth, ve diğer yetkilendirme ayarlarÄąnÄą yÃļnet", @@ -44,7 +45,7 @@ "backup_database_enable_description": "VeritabanÄą yığınlarÄąnÄą etkinleştir", "backup_keep_last_amount": "TutulmasÄą gereken geçmiş yığınÄą miktarÄą", "backup_settings": "VeritabanÄą yığınÄą ayarlarÄą", - "backup_settings_description": "VeritabanÄą Yedekleme AyarlarÄąnÄą YÃļnet", + "backup_settings_description": "VeritabanÄą dÃļkÃŧm ayarlarÄąnÄą yÃļnet.", "cleared_jobs": "{job} için işler temizlendi", "config_set_by_file": "Ayarlar şuanda config dosyasÄą tarafÄąndan ayarlanmÄąÅŸtÄąr", "confirm_delete_library": "{library} kÃŧtÃŧphanesini silmek istediğinize emin misiniz?", @@ -68,7 +69,9 @@ "force_delete_user_warning": "UYARI: Bu işlem kullanÄącÄąyÄą ve tÃŧm varlÄąklarÄą anÄąnda kaldÄąracaktÄąr. Bu geri alÄąnamaz ve dosyalar geri getirilemez.", "image_format": "Biçim", "image_format_description": "WebP, JPEG'e gÃļre daha kÃŧçÃŧk dosya boyutu sunar fakat işlemesi daha uzun sÃŧrer.", + "image_fullsize_description": "YakÄąnlaştÄąrÄąldığında kullanÄąlan, meta verileri kaldÄąrÄąlmÄąÅŸ tam boyutlu gÃļrÃŧntÃŧ", "image_fullsize_enabled": "Tam boyutlu gÃļrÃŧntÃŧ Ãŧretimini etkinleştir", + "image_fullsize_enabled_description": "Yerleşik Ãļnizlemeyi tercih et” seçeneği etkinleştirildiğinde, yerleşik Ãļnizlemeler dÃļnÃŧştÃŧrme yapÄąlmadan doğrudan kullanÄąlÄąr. JPEG gibi web dostu formatlar bu ayardan etkilenmez.", "image_fullsize_quality_description": "1-100 arasÄąnda tam boyutlu gÃļrÃŧntÃŧ kalitesi. Daha yÃŧksek kalitelidir, ancak daha bÃŧyÃŧk dosyalar Ãŧretir.", "image_fullsize_title": "Tam boyutlu gÃļrÃŧntÃŧ ayarlarÄą", "image_prefer_embedded_preview": "GÃļmÃŧlÃŧ Ãļnizlemeyi tercih et", @@ -168,7 +171,7 @@ "note_apply_storage_label_previous_assets": "Not: Daha Ãļnce yÃŧklenen varlÄąklara Depolama Etiketi uygulamak için şu komutu çalÄąÅŸtÄąrÄąn", "note_cannot_be_changed_later": "NOT: Bu daha sonra değiştirilemez!", "notification_email_from_address": "Şu adresten", - "notification_email_from_address_description": "GÃļndericinin email adresi, Ãļrnek: \"Immich Fotoğraf Sunucusu \"", + "notification_email_from_address_description": "GÃļnderen e-posta adresi, Ãļrneğin: \"Immich GÃļrsel Sunucusu \". E-posta gÃļnderilmesine izin verdiğiniz bir adres kullandığınÄązdan emin olun.", "notification_email_host_description": "E-posta sunucusunun ana bilgisayarÄą (Ãļrneğin, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Sertifika hatalarÄąnÄą gÃļrmezden gel", "notification_email_ignore_certificate_errors_description": "TLS sertifika doğrulama ayarlarÄąnÄą gÃļrmezden gel (Önerilmez)", @@ -188,6 +191,7 @@ "oauth_auto_register": "Otomatik kayÄąt", "oauth_auto_register_description": "OAuth ile giriş yapan yeni kullanÄącÄąlarÄą otomatik kaydet", "oauth_button_text": "Buton yazÄąsÄą", + "oauth_client_secret_description": "OAuth sağlayÄącÄąsÄą PKCE (Kod Değişimi İçin KanÄąt AnahtarÄą) desteği sunmuyorsa gereklidir", "oauth_enable_description": "OAuth ile giriş yap", "oauth_mobile_redirect_uri": "Mobil yÃļnlendirme URL'si", "oauth_mobile_redirect_uri_override": "Mobilde zorla kullanÄąlacak YÃļnlendirme Adresi", @@ -200,7 +204,7 @@ "oauth_storage_quota_claim": "Depolama kotasÄą talebi", "oauth_storage_quota_claim_description": "KullanÄącÄąya depolama kotasÄą koymak için kullanÄąlacak değer (en: OAuth claim).", "oauth_storage_quota_default": "VarsayÄąlan depolama kotasÄą (GiB)", - "oauth_storage_quota_default_description": "Değer (en: OAuth claim) mevcut değilse konulacak kota. GiB cinsinden, sÄąnÄąrsÄąz kota için 0 kullanÄąn.", + "oauth_storage_quota_default_description": "Değer (en: OAuth claim) mevcut değilse GiB cinsinden konulacak kota.", "oauth_timeout": "İstek zaman aÅŸÄąmÄą", "oauth_timeout_description": "Milisaniye cinsinden istek zaman aÅŸÄąmÄą", "password_enable_description": "Email ve şifre ile giriş yap", @@ -237,9 +241,10 @@ "storage_template_hash_verification_enabled_description": "Hash doğrulamayÄą etkinleştirir, eğer ne işe yaradığınÄą bilmiyorsanÄąz bunu devre dÄąÅŸÄą bÄąrakmayÄąn", "storage_template_migration": "Depolama şablonu birleştirme", "storage_template_migration_description": "Geçerli {template} ayarlarÄąnÄą daha Ãļnce yÃŧklenmiş olan varlÄąklara uygula", - "storage_template_migration_info": "Şablon ayarlarÄąndaki değişiklikler sadece yeni varlÄąklara uygulanacak. Şablon ayarlarÄąnÄą daha Ãļnce yÃŧklenmiş olan varlÄąklara uygulamak için {job} çalÄąÅŸtÄąrÄąn.", + "storage_template_migration_info": "Depolama şablonu tÃŧm dosya uzantÄąlarÄąnÄą kÃŧçÃŧk harfe dÃļnÃŧştÃŧrecektir. Şablon ayarlarÄąndaki değişiklikler sadece yeni varlÄąklara uygulanacak. Şablon ayarlarÄąnÄą daha Ãļnce yÃŧklenmiş olan varlÄąklara uygulamak için {job} çalÄąÅŸtÄąrÄąn.", "storage_template_migration_job": "Depolama Adreslerini Değiştirme GÃļrevi", "storage_template_more_details": "Bu Ãļzellik hakkÄąnda daha fazla bilgi için, Depolama Şablonu ve onun etkileri kÄąsmÄąna bakÄąn", + "storage_template_onboarding_description_v2": "Etkinleştirildiğinde, bu Ãļzellik dosyalarÄą kullanÄącÄą tanÄąmlÄą bir şablona gÃļre otomatik olarak organize eder. Daha fazla bilgi için lÃŧtfen belgelere bakÄąn.", "storage_template_path_length": "Tahmini dosya adresi uzunluğu: {length, number}/{limit, number}", "storage_template_settings": "Depolama Şablonu", "storage_template_settings_description": "YÃŧklenen dosyanÄąn ismini ve klasÃļr yapÄąsÄąnÄą dÃŧzenle", @@ -254,7 +259,7 @@ "template_email_update_album": "AlbÃŧm Şablonunu GÃŧncelle", "template_email_welcome": "Hoş geldiniz e-posta şablonu", "template_settings": "Bildirim ŞablonlarÄą", - "template_settings_description": "Bildirim şablonlarÄąnÄą yÃļnet.", + "template_settings_description": "Bildirim şablonlarÄąnÄą yÃļnet", "theme_custom_css_settings": "Özel CSS", "theme_custom_css_settings_description": "CSS (Cascading Style Sheets) kullanÄąlarak Immich'in tasarÄąmÄą değiştirilebilir.", "theme_settings": "Tema ayarlarÄą", @@ -286,13 +291,13 @@ "transcoding_encoding_options": "Kodlama Seçenekleri", "transcoding_encoding_options_description": "KodlanmÄąÅŸ videolar için kodekleri, çÃļzÃŧnÃŧrlÃŧğÃŧ, kaliteyi ve diğer seçenekleri ayarlayÄąn", "transcoding_hardware_acceleration": "DonanÄąm HÄązlandÄąrma", - "transcoding_hardware_acceleration_description": "Deneysel; daha hÄązlÄą, fakat aynÄą bitrate ayarlarÄąnda daha dÃŧşÃŧk kaliteye sahip", + "transcoding_hardware_acceleration_description": "Deneysel: daha hÄązlÄą dÃļnÃŧştÃŧrme, ancak aynÄą bit hÄązÄąnda kaliteyi dÃŧşÃŧrebilir", "transcoding_hardware_decoding": "DonanÄąm çÃļzÃŧcÃŧ", "transcoding_hardware_decoding_setting_description": "Uçtan uca hÄązlandÄąrmayÄą, sadece kodlamayÄą hÄązlandÄąrmanÄąn yerine etkinleştirir. TÃŧm videolarda çalÄąÅŸmayabilir.", "transcoding_max_b_frames": "Maksimum B-kareler", "transcoding_max_b_frames_description": "Daha yÃŧksek değerler sÄąkÄąÅŸtÄąrma verimliliğini artÄąrÄąr, ancak kodlamayÄą yavaşlatÄąr. Eski cihazlarda donanÄąm hÄązlandÄąrma ile uyumlu olmayabilir. 0, B-çerçevelerini devre dÄąÅŸÄą bÄąrakÄąr, -1 ise bu değeri otomatik olarak ayarlar.", "transcoding_max_bitrate": "Maksimum bitrate", - "transcoding_max_bitrate_description": "Maksimum bit hÄązÄą ayarlamak, kaliteye kÃŧçÃŧk bir maliyetle dosya boyutlarÄąnÄą daha ÃļngÃļrÃŧlebilir hale getirebilir.", + "transcoding_max_bitrate_description": "Maksimum bit hÄązÄą ayarlamak, kaliteyi az bir maliyetle dÃŧşÃŧrerek dosya boyutlarÄąnÄą daha ÃļngÃļrÃŧlebilir hale getirebilir. 720p çÃļzÃŧnÃŧrlÃŧkte, tipik değerler VP9 veya HEVC için 2600 kbit/s, H.264 için ise 4500 kbit/s’dir. 0 olarak ayarlanÄąrsa devre dÄąÅŸÄą bÄąrakÄąlÄąr.", "transcoding_max_keyframe_interval": "Maksimum ana kare aralığı", "transcoding_max_keyframe_interval_description": "Ana kareler arasÄąndaki maksimum kare mesafesini ayarlar. DÃŧşÃŧk değerler sÄąkÄąÅŸtÄąrma verimliliğini kÃļtÃŧleştirir, ancak arama sÃŧrelerini iyileştirir ve hÄązlÄą hareket içeren sahnelerde kaliteyi artÄąrabilir. 0 bu değeri otomatik olarak ayarlar.", "transcoding_optimal_description": "Hedef çÃļzÃŧnÃŧrlÃŧkten yÃŧksek veya kabul edilen formatta olmayan videolar", @@ -352,6 +357,7 @@ "admin_password": "YÃļnetici Şifresi", "administration": "YÃļnetim", "advanced": "Gelişmiş", + "advanced_settings_enable_alternate_media_filter_subtitle": "Eşleme sÄąrasÄąnda medyayÄą alternatif ÃļlçÃŧtlere gÃļre sÃŧzgeçten geçirmek için bu seçeneği kullanÄąn. UygulamanÄąn tÃŧm albÃŧmleri algÄąlamasÄąnda sorun yaÅŸÄąyorsanÄąz yalnÄązca bu durumda deneyin.", "advanced_settings_enable_alternate_media_filter_title": "[DENEYSEL] Alternatif cihaz albÃŧm eşleme sÃŧzgeci kullanÄąn", "advanced_settings_log_level_title": "GÃŧnlÃŧk dÃŧzeyi: {level}", "advanced_settings_prefer_remote_subtitle": "BazÄą cihazlar, cihazdaki Ãļğelerin kÃŧçÃŧk resimlerini gÃļstermekte çok yavaştÄąr. Bunun yerine sunucudaki kÃŧçÃŧk resimleri gÃļstermek için bu ayarÄą etkinleştirin.", @@ -360,6 +366,7 @@ "advanced_settings_proxy_headers_title": "Proxy Header'lar", "advanced_settings_self_signed_ssl_subtitle": "Sunucu uç noktasÄą için SSL sertifika doğrulamasÄąnÄą atlar. Kendinden imzalÄą sertifikalar için gereklidir.", "advanced_settings_self_signed_ssl_title": "Kendi kendine imzalanmÄąÅŸ SSL sertifikalarÄąna izin ver", + "advanced_settings_sync_remote_deletions_subtitle": "Web Ãŧzerinde işlem yapÄąldığında, bu aygÄąttaki varlığı otomatik olarak sil veya geri yÃŧkle", "advanced_settings_sync_remote_deletions_title": "Uzaktan silinmeleri eşle [DENEYSEL]", "advanced_settings_tile_subtitle": "Gelişmiş kullanÄącÄą ayarlarÄą", "advanced_settings_troubleshooting_subtitle": "Sorun giderme için ek Ãļzellikleri etkinleştirin", @@ -392,11 +399,14 @@ "album_viewer_appbar_share_err_remove": "AlbÃŧmden Ãļğeleri kaldÄąrmada sorunlar var", "album_viewer_appbar_share_err_title": "AlbÃŧm başlığı değiştirilemedi", "album_viewer_appbar_share_leave": "AlbÃŧmden Ã§Äąk", - "album_viewer_appbar_share_to": "Paylaş:", + "album_viewer_appbar_share_to": "Paylaşma", "album_viewer_page_share_add_users": "KullanÄącÄą ekle", "album_with_link_access": "Link'e sahip olan herhangi bir kişinin bu albÃŧmdeki fotoğraflarÄą ve kişileri gÃļrmesine izin ver.", "albums": "AlbÃŧmler", "albums_count": "{count, plural, one {{count, number} AlbÃŧm} other {{count, number} AlbÃŧm}}", + "albums_default_sort_order": "VarsayÄąlan albÃŧm sÄąralama dÃŧzeni", + "albums_default_sort_order_description": "Yeni albÃŧm oluştururken kullanÄąlacak başlangÄąÃ§ varlÄąk sÄąralama dÃŧzeni.", + "albums_feature_description": "Diğer kullanÄącÄąlarla paylaÅŸÄąlabilen varlÄąk koleksiyonlarÄą.", "all": "TÃŧmÃŧ", "all_albums": "TÃŧm AlbÃŧmler", "all_people": "TÃŧm Kişiler", @@ -417,6 +427,7 @@ "app_settings": "Uygulama AyarlarÄą", "appears_in": "Şurada gÃļrÃŧnÃŧr", "archive": "Arşiv", + "archive_action_prompt": "{count} arşive eklendi", "archive_or_unarchive_photo": "FotoğrafÄą arşivle/arşivden Ã§Äąkar", "archive_page_no_archived_assets": "Arşivlenmiş Ãļğe bulunamadÄą", "archive_page_title": "Arşiv ({count})", @@ -454,10 +465,12 @@ "assets": "VarlÄąklar", "assets_added_count": "{count, plural, one {# varlÄąk eklendi} other {# varlÄąk eklendi}}", "assets_added_to_album_count": "{count, plural, one {# varlÄąk} other {# varlÄąk}} albÃŧme eklendi", - "assets_added_to_name_count": "{count, plural, one {# varlÄąk} other {# varlÄąk}} {hasName, select, true {{name}} other {yeni albÃŧm}} içine eklendi", + "assets_cannot_be_added_to_album_count": "{count, plural, one {VarlÄąk} other {VarlÄąklar}} albÃŧme eklenemiyor", "assets_count": "{count, plural, one {# varlÄąk} other {# varlÄąklar}}", "assets_deleted_permanently": "{count} Ãļğe kalÄącÄą olarak silindi", "assets_deleted_permanently_from_server": "{count} Ãļğe kalÄącÄą olarak Immich sunucusundan silindi", + "assets_downloaded_failed": "{count, plural, one {# dosya indirildi – {error} dosya indirilemedi} other {# dosya indirildi – {error} dosya indirilemedi}}", + "assets_downloaded_successfully": "{count, plural, one {# dosya başarÄąyla indirildi} other {# dosya başarÄąyla indirildi}}", "assets_moved_to_trash_count": "{count, plural, one {# varlÄąk} other {# varlÄąk}} çÃļpe taÅŸÄąndÄą", "assets_permanently_deleted_count": "KalÄącÄą olarak silindi {count, plural, one {# varlÄąk} other {# varlÄąklar}}", "assets_removed_count": "KaldÄąrÄąldÄą {count, plural, one {# varlÄąk} other {# varlÄąklar}}", @@ -484,12 +497,12 @@ "backup_album_selection_page_selection_info": "Seçim Bilgileri", "backup_album_selection_page_total_assets": "Toplam eşsiz Ãļğeler", "backup_all": "TÃŧmÃŧ", - "backup_background_service_backup_failed_message": "Yedekleme başarÄąsÄąz. Tekrar deneniyor...", - "backup_background_service_connection_failed_message": "Sunucuya bağlanÄąlamadÄą. Tekrar deneniyor...", + "backup_background_service_backup_failed_message": "Yedekleme başarÄąsÄąz. Tekrar deneniyorâ€Ļ", + "backup_background_service_connection_failed_message": "Sunucuya bağlanÄąlamadÄą. Tekrar deneniyorâ€Ļ", "backup_background_service_current_upload_notification": "{filename} yÃŧkleniyor", "backup_background_service_default_notification": "Yeni Ãļğeler kontrol ediliyorâ€Ļ", "backup_background_service_error_title": "Yedekleme hatasÄą", - "backup_background_service_in_progress_notification": "Öğeleriniz yedekleniyor...", + "backup_background_service_in_progress_notification": "Öğeleriniz yedekleniyorâ€Ļ", "backup_background_service_upload_failure_notification": "{filename} yÃŧklemesi başarÄąsÄąz oldu", "backup_controller_page_albums": "Yedekleme AlbÃŧmleri", "backup_controller_page_background_app_refresh_disabled_content": "Arka planda yedeklemeyi kullanabilmek için Ayarlar > Genel > Arka Planda Uygulama Yenileme bÃļlÃŧmÃŧnden arka planda uygulama yenilemeyi etkinleştirin.", @@ -577,6 +590,8 @@ "cannot_merge_people": "Kişiler birleştirilemiyor", "cannot_undo_this_action": "Bu işlem geri alÄąnamaz!", "cannot_update_the_description": "AÃ§Äąklama gÃŧncellenemiyor", + "cast": "YansÄąt", + "cast_description": "KullanÄąlabilir yansÄątma hedeflerini yapÄąlandÄąr", "change_date": "Tarihi değiştir", "change_description": "AÃ§ÄąklamayÄą değiştir", "change_display_order": "GÃļrÃŧntÃŧleme sÄąrasÄąnÄą değiştir", @@ -636,6 +651,7 @@ "confirm_tag_face": "Bu yÃŧzÃŧ {name} olarak etiketlemek ister misiniz?", "confirm_tag_face_unnamed": "Bu yÃŧzÃŧ etiketlemek ister misin?", "connected_device": "Cihaz bağlandÄą", + "connected_to": "BağlÄą", "contain": "İçermek", "context": "Bağlam", "continue": "Devam et", @@ -644,8 +660,8 @@ "control_bottom_app_bar_delete_from_local": "Cihazdan sil", "control_bottom_app_bar_edit_location": "Konumu DÃŧzenle", "control_bottom_app_bar_edit_time": "Tarih ve Saati DÃŧzenle", - "control_bottom_app_bar_share_link": "İlişimi paylaş", - "control_bottom_app_bar_share_to": "Paylaş:", + "control_bottom_app_bar_share_link": "BağlantÄąyÄą Paylaş", + "control_bottom_app_bar_share_to": "Paylaşma", "control_bottom_app_bar_trash_from_immich": "ÇÃļp Kutusuna At", "copied_image_to_clipboard": "Resim, panoya kopyalandÄą.", "copied_to_clipboard": "Panoya kopyalandÄą!", @@ -687,6 +703,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ç", "date_after": "Sonraki tarih", "date_and_time": "Tarih ve Zaman", "date_before": "Önceki tarih", @@ -702,6 +719,7 @@ "default_locale": "VarsayÄąlan Yerel Ayar", "default_locale_description": "Tarihleri ve sayÄąlarÄą tarayÄącÄąnÄązÄąn yerel ayarÄąna gÃļre biçimlendirin", "delete": "Sil", + "delete_action_prompt": "{count} kalÄącÄą olarak silindi", "delete_album": "AlbÃŧmÃŧ sil", "delete_api_key_prompt": "Bu API anahtarÄąnÄą silmek istediğinizden emin misiniz?", "delete_dialog_alert": "Bu Ãļğeler cihazÄąnÄązdan ve Immich'ten kalÄącÄą olarak silinecektir", @@ -715,6 +733,7 @@ "delete_key": "AnahtarÄą sil", "delete_library": "KÃŧtÃŧphaneyi sil", "delete_link": "BağlantÄąyÄą sil", + "delete_local_action_prompt": "{count} yerel olarak silindi", "delete_local_dialog_ok_backed_up_only": "Sadece Yedeklenmişleri Sil", "delete_local_dialog_ok_force": "Yine de Sil", "delete_others": "Diğerlerini sil", @@ -734,6 +753,7 @@ "disallow_edits": "Değişikliklere izin verme", "discord": "Discord", "discover": "Keşfet", + "discovered_devices": "Keşfedilen aygÄątlar", "dismiss_all_errors": "TÃŧm hatalarÄą yoksay", "dismiss_error": "HatayÄą yoksay", "display_options": "GÃļrÃŧntÃŧleme seçenekleri", @@ -802,6 +822,7 @@ "enable_biometric_auth_description": "Biyometrik kimlik doğrulamasÄąnÄą etkinleştirmek için PIN kodu girin", "enabled": "Etkinleştirildi", "end_date": "Bitiş tarihi", + "enqueued": "Kuyruğa alÄąndÄą", "enter_wifi_name": "Wi-Fi adÄąnÄą girin", "enter_your_pin_code": "Pin kodu girin", "enter_your_pin_code_subtitle": "Kilitli klasÃļre erişmek için PIN kodunuzu girin", @@ -810,6 +831,7 @@ "error_delete_face": "YÃŧzÃŧ varlÄąktan silme hatasÄą", "error_loading_image": "Resim yÃŧklenirken hata oluştu", "error_saving_image": "Hata: {error}", + "error_tag_face_bounding_box": "YÃŧz etiketleme hatasÄą – sÄąnÄąrlayÄącÄą kutu koordinatlarÄą alÄąnamadÄą", "error_title": "Bir Hata Oluştu - Bir şeyler ters gitti", "errors": { "cannot_navigate_next_asset": "Sonraki varlığa geçiş yapÄąlamÄąyor", @@ -859,6 +881,7 @@ "unable_to_archive_unarchive": "{archived, select, true {Arşivleme} other {Arşivden Ã§Äąkarma}} işlemi yapÄąlamÄąyor", "unable_to_change_album_user_role": "AlbÃŧm kullanÄącÄą rolÃŧ değiştirilemiyor", "unable_to_change_date": "Tarih değiştirilemiyor", + "unable_to_change_description": "AÃ§Äąklama değiştirilemiyor", "unable_to_change_favorite": "Favori durumu değiştirilemiyor", "unable_to_change_location": "Konum değiştirilemiyor", "unable_to_change_password": "Şifre değiştirilemiyor", @@ -902,6 +925,7 @@ "unable_to_remove_partner": "Ortak kaldÄąrÄąlamÄąyor", "unable_to_remove_reaction": "Reaksiyon kaldÄąrÄąlamÄąyor", "unable_to_reset_password": "Şifre sÄąfÄąrlanamÄąyor", + "unable_to_reset_pin_code": "Pin kodunu sÄąfÄąrlanamÄąyor", "unable_to_resolve_duplicate": "Çiftler çÃļzÃŧmlenemiyor", "unable_to_restore_assets": "VarlÄąklar geri yÃŧklenemiyor", "unable_to_restore_trash": "ÇÃļp geri yÃŧklenemiyor", @@ -935,6 +959,9 @@ "exif_bottom_sheet_location": "KONUM", "exif_bottom_sheet_people": "KİŞİLER", "exif_bottom_sheet_person_add_person": "İsim ekle", + "exif_bottom_sheet_person_age_months": "Yaş: {months} ay", + "exif_bottom_sheet_person_age_year_months": "Yaş: 1 yÄąl, {months} ay", + "exif_bottom_sheet_person_age_years": "Yaş: {years}", "exit_slideshow": "Slayt gÃļsterisinden Ã§Äąk", "expand_all": "Hepsini genişlet", "experimental_settings_new_asset_list_subtitle": "ÇalÄąÅŸmalar devam ediyor", @@ -952,13 +979,17 @@ "external": "Harici", "external_libraries": "Harici kÃŧtÃŧphaneler", "external_network": "Harici ağlar", - "external_network_sheet_info": "Belirlenmiş WiFi ağına bağlÄą olmadığında uygulama, yukarÄądan aşağıya doğru ulaşabileceği aşağıdaki URL'lerden ilki aracÄąlığıyla sunucuya bağlanacaktÄąr", + "external_network_sheet_info": "Belirlenmiş Wi-Fi ağına bağlÄą olmadığında uygulama, yukarÄądan aşağıya doğru ulaşabileceği aşağıdaki URL'lerden ilki aracÄąlığıyla sunucuya bağlanacaktÄąr", "face_unassigned": "YÃŧz atanmadÄą", + "failed": "BaşarÄąsÄąz", + "failed_to_authenticate": "Kimlik doğrulamasÄą yapÄąlamadÄą", "failed_to_load_assets": "VarlÄąklar yÃŧklenemedi", - "favorite": "Favori", - "favorite_or_unfavorite_photo": "Favoriye ekle veya Ã§Äąkar", - "favorites": "Favoriler", - "favorites_page_no_favorites": "Favori Ãļğe bulunamadÄą", + "failed_to_load_folder": "KlasÃļr yÃŧklenemedi", + "favorite": "GÃļzde", + "favorite_action_prompt": "{count} gÃļzdelere eklendi", + "favorite_or_unfavorite_photo": "GÃļzdeye ekle veya Ã§Äąkar", + "favorites": "GÃļzdeler", + "favorites_page_no_favorites": "GÃļzde Ãļge bulunamadÄą", "feature_photo_updated": "Özellikli fotoğraf gÃŧncellendi", "features": "Özellikler", "features_setting_description": "UygulamanÄąn Ãļzelliklerini yÃļnet", @@ -968,11 +999,16 @@ "filetype": "Dosya tipi", "filter": "Filtre", "filter_people": "Kişileri filtrele", + "filter_places": "Yerleri sÃŧz", "find_them_fast": "AdlarÄąna gÃļre hÄązlÄąca bul", "fix_incorrect_match": "YanlÄąÅŸ eşleştirmeyi dÃŧzelt", + "folder": "KlasÃļr", + "folder_not_found": "KlasÃļr bulunamadÄą", "folders": "KlasÃļrler", "folders_feature_description": "Dosya sistemindeki fotoğraf ve videolarÄą klasÃļr gÃļrÃŧnÃŧmÃŧyle keşfedin", "forward": "İleri", + "gcast_enabled": "Google Cast", + "gcast_enabled_description": "Bu Ãļzellik, çalÄąÅŸabilmek için Google'dan harici kaynaklar yÃŧkler.", "general": "Genel", "get_help": "YardÄąm Al", "get_wifiname_error": "Wi-Fi adÄą alÄąnamadÄą. Gerekli izinleri verdiğinizden ve bir Wi-Fi ağına bağlÄą olduğunuzdan emin olun", @@ -1012,12 +1048,16 @@ "home_page_building_timeline": "Zaman çizelgesi oluşturuluyor", "home_page_delete_err_partner": "Partner Ãļğeleri silinemez, atlanÄąyor", "home_page_delete_remote_err_local": "Uzaktan silme seçimindeki yerel Ãļğeler atlanÄąyor", - "home_page_favorite_err_local": "Yerel Ãļğeler henÃŧz favorilere eklenemiyor, atlanÄąyor", - "home_page_favorite_err_partner": "Partner Ãļğeleri henÃŧz favorilere eklenemiyor, atlanÄąyor", + "home_page_favorite_err_local": "Yerel Ãļgeler henÃŧz gÃļzdelere eklenemiyor, atlanÄąyor", + "home_page_favorite_err_partner": "Ortak Ãļgeleri henÃŧz gÃļzdelere eklenemiyor, atlanÄąyor", "home_page_first_time_notice": "UygulamayÄą ilk kez kullanÄąyorsanÄąz, zaman çizelgesinin albÃŧmlerdeki fotoğraf ve videolar ile oluşturulabilmesi için lÃŧtfen yedekleme için albÃŧm(ler) seçtiğinizden emin olun.", + "home_page_locked_error_local": "Yerel varlÄąklar kilitli klasÃļre taÅŸÄąnamÄąyor, atlanÄąyor", + "home_page_locked_error_partner": "Ortak varlÄąklar kilitli klasÃļre taÅŸÄąnamÄąyor, atlanÄąyor", "home_page_share_err_local": "Yerel Ãļğeler bağlantÄą ile paylaÅŸÄąlamaz, atlanÄąyor", "home_page_upload_err_limit": "AynÄą anda en fazla 30 Ãļğe yÃŧklenebilir, atlanabilir", + "host": "Ana bilgisayar", "hour": "Saat", + "id": "ID", "ignore_icloud_photos": "iCloud FotoğraflarÄąnÄą Yok Say", "ignore_icloud_photos_description": "iCloud'a yÃŧklenmiş fotoğraflar Immich sunucusuna yÃŧklenmesin", "image": "Resim", @@ -1057,6 +1097,12 @@ "invalid_date_format": "Geçersiz tarih formatÄą", "invite_people": "Kişileri Davet Et", "invite_to_album": "AlbÃŧme davet et", + "ios_debug_info_fetch_ran_at": "Veri çekme {dateTime} tarihinde çalÄąÅŸtÄąrÄąldÄą", + "ios_debug_info_last_sync_at": "Son eşleme: {dateTime}", + "ios_debug_info_no_processes_queued": "Hiçbir arka plan işlemi kuyruğa alÄąnmadÄą", + "ios_debug_info_no_sync_yet": "HenÃŧz hiçbir arka plan eşleme gÃļrevi çalÄąÅŸtÄąrÄąlmadÄą", + "ios_debug_info_processes_queued": "{count, plural, one {{count} arka plan işlemi kuyruğa alÄąndÄą} other {{count} arka plan işlemi kuyruğa alÄąndÄą}}", + "ios_debug_info_processing_ran_at": "İşleme {dateTime} tarihinde çalÄąÅŸtÄąrÄąldÄą", "items_count": "{count, plural, one {# Öğe} other {# Öğe}}", "jobs": "GÃļrevler", "keep": "Koru", @@ -1065,6 +1111,9 @@ "kept_this_deleted_others": "Bu varlÄąk tutuldu ve {count, plural, one {# varlÄąk} other {# varlÄąk}} silindi", "keyboard_shortcuts": "Klavye kÄąsayollarÄą", "language": "Dil", + "language_no_results_subtitle": "Arama teriminizi değiştirmeyi deneyin", + "language_no_results_title": "Dil bulunamadÄą", + "language_search_hint": "Dilleri ara...", "language_setting_description": "Tercih ettiğiniz dili seçiniz", "last_seen": "Son gÃļrÃŧlme", "latest_version": "En son versiyon", @@ -1081,6 +1130,7 @@ "library_page_sort_created": "Oluşturma tarihi", "library_page_sort_last_modified": "Son dÃŧzenleme", "library_page_sort_title": "AlbÃŧm başlığı", + "licenses": "Lisanslar", "light": "AÃ§Äąk", "like_deleted": "Beğeni silindi", "link_motion_video": "Hareket videosunu bağla", @@ -1090,17 +1140,21 @@ "list": "Liste", "loading": "YÃŧkleniyor", "loading_search_results_failed": "Arama sonuçlarÄą yÃŧklenemedi", + "local_asset_cast_failed": "Sunucuya yÃŧklenmemiş bir varlÄąk yansÄątÄąlamaz", "local_network": "Yerel Wi-Fi", "local_network_sheet_info": "Uygulama belirlenmiş Wi-Fi ağınÄą kullanÄąrken bu URL Ãŧzerinden sunucuya bağlanacaktÄąr", "location_permission": "Konum izni", - "location_permission_content": "Otomatik geçiş Ãļzelliğinin çalÄąÅŸabilmesi için Immich'in mevcut Wi-Fi ağınÄąn adÄąnÄą bilmesi, bunu sağlamak için de tam konum iznine ihtiyacÄą vardÄąr.", + "location_permission_content": "Otomatik geçiş Ãļzelliğinin çalÄąÅŸabilmesi için Immich'in mevcut Wi-Fi ağınÄąn adÄąnÄą bilmesi, bunu sağlamak için de tam konum iznine ihtiyacÄą vardÄąr", "location_picker_choose_on_map": "Haritada seç", "location_picker_latitude_error": "Geçerli bir enlem yazÄąn", "location_picker_latitude_hint": "Buraya enlem yazÄąn", "location_picker_longitude_error": "Geçerli bir boylam yazÄąn", "location_picker_longitude_hint": "Buraya boylam yazÄąn", + "lock": "Kilitle", + "locked_folder": "Kilitli KlasÃļr", "log_out": "Oturumu kapat", "log_out_all_devices": "TÃŧm Cihazlarda Oturumu Kapat", + "logged_in_as": "{user} olarak oturum aÃ§ÄąldÄą", "logged_out_all_devices": "TÃŧm cihazlarda oturum kapatÄąldÄą", "logged_out_device": "Oturum kapatÄąlmÄąÅŸ cihaz", "login": "Giriş yap", @@ -1124,7 +1178,7 @@ "login_form_server_empty": "Sunucu URL'si girin", "login_form_server_error": "Sunucuya bağlanÄąlamadÄą.", "login_has_been_disabled": "Giriş devre dÄąÅŸÄą bÄąrakÄąldÄą.", - "login_password_changed_error": "Parola gÃŧncellenirken bir hata oluştu.", + "login_password_changed_error": "ParolanÄąz gÃŧncellenirken bir hata oluştu.", "login_password_changed_success": "Parola gÃŧncellendi", "logout_all_device_confirmation": "TÃŧm cihazlarda oturum kapatmak istediğinizden emin misiniz?", "logout_this_device_confirmation": "Bu cihazda oturum kapatmak istediğinizden emin misiniz?", @@ -1133,6 +1187,7 @@ "loop_videos": "VideolarÄą dÃļngÃŧye al", "loop_videos_description": "AyrÄąntÄą gÃļrÃŧnÃŧmÃŧnde videolarÄąn otomatik dÃļngÃŧye alÄąnmasÄąnÄą etkinleştir.", "main_branch_warning": "Geliştirme sÃŧrÃŧmÃŧ kullanÄąyorsunuz. YayÄąnlanan bir sÃŧrÃŧm kullanmanÄązÄą Ãļnemle tavsiye ederiz!", + "main_menu": "Ana menÃŧ", "make": "Marka", "manage_shared_links": "PaylaÅŸÄąlan bağlantÄąlarÄą yÃļnet", "manage_sharing_with_partners": "Ortaklarla paylaÅŸÄąmÄą yÃļnet", @@ -1163,9 +1218,12 @@ "map_settings_dialog_title": "Harita AyarlarÄą", "map_settings_include_show_archived": "Arşivdekileri dahil et", "map_settings_include_show_partners": "Partnerleri Dahil Et", - "map_settings_only_show_favorites": "Sadece Favorileri GÃļster", + "map_settings_only_show_favorites": "Sadece GÃļzdeleri GÃļster", "map_settings_theme_settings": "Harita TemasÄą", "map_zoom_to_see_photos": "FotoğraflarÄą gÃļrmek için uzaklaştÄąrÄąn", + "mark_all_as_read": "TÃŧmÃŧnÃŧ okundu olarak işaretle", + "mark_as_read": "Okundu olarak işaretle", + "marked_all_as_read": "TÃŧmÃŧ okundu olarak işaretlendi", "matches": "Eşleşenler", "media_type": "Medya tÃŧrÃŧ", "memories": "AnÄąlar", @@ -1186,8 +1244,16 @@ "minimize": "KÃŧçÃŧlt", "minute": "Dakika", "missing": "Eksik", + "model": "Model", "month": "Ay", + "monthly_title_text_date_format": "MMMM y", "more": "Daha fazla", + "move": "TaÅŸÄą", + "move_off_locked_folder": "Kilitli klasÃļrden taÅŸÄą", + "move_to_locked_folder": "Kilitli klasÃļre taÅŸÄą", + "move_to_locked_folder_confirmation": "Bu fotoğraflar ve videolar tÃŧm albÃŧmlerden kaldÄąrÄąlacak ve yalnÄązca kilitli klasÃļrden gÃļrÃŧntÃŧlenebilecektir", + "moved_to_archive": "{count, plural, one {# Ãļğe arşive taÅŸÄąndÄą} other {# Ãļğe arşive taÅŸÄąndÄą}}", + "moved_to_library": "{count, plural, one {# Ãļğe kitaplığa taÅŸÄąndÄą} other {# Ãļğe kitaplığa taÅŸÄąndÄą}}", "moved_to_trash": "ÇÃļp kutusuna taÅŸÄąndÄą", "multiselect_grid_edit_date_time_err_read_only": "Salt okunur Ãļğelerin tarihi dÃŧzenlenemedi, atlanÄąyor", "multiselect_grid_edit_gps_err_read_only": "Salt okunur Ãļğelerin konumu dÃŧzenlenemedi, atlanÄąyor", @@ -1203,6 +1269,7 @@ "new_password": "Yeni şifre", "new_person": "Yeni kişi", "new_pin_code": "Yeni PIN kodu", + "new_pin_code_subtitle": "Kilitli klasÃļre ilk kez erişiyorsunuz. Bu sayfaya gÃŧvenli erişim için bir PIN kodu oluşturun", "new_user_created": "Yeni kullanÄącÄą oluşturuldu", "new_version_available": "YENİ VERSİYON MEVCUT", "newest_first": "Önce en yeniler", @@ -1215,19 +1282,25 @@ "no_archived_assets_message": "Fotoğraf gÃļrÃŧnÃŧmÃŧnÃŧzden kaldÄąrmak için fotoğraflarÄą ve videolarÄą arşivleyin", "no_assets_message": "İLK FOTOĞRAFINIZI YÜKLEMEK İÇİN TIKLAYIN", "no_assets_to_show": "GÃļsterilecek Ãļğe yok", + "no_cast_devices_found": "YansÄątÄąlacak cihaz bulunamadÄą", "no_duplicates_found": "Çift bulunamadÄą.", "no_exif_info_available": "EXIF bilgisi mevcut değil", "no_explore_results_message": "Koleksiyonunuzu keşfetmek için daha fazla fotoğraf yÃŧkleyin.", - "no_favorites_message": "En sevdiğiniz fotoğraf ve videolarÄą hÄązlÄąca bulmak için favoriler ekleyin", + "no_favorites_message": "En sevdiğiniz fotoğraf ve videolarÄą hÄązlÄąca bulmak için gÃļzdelere ekleyin", "no_libraries_message": "Fotoğraf ve videolarÄąnÄązÄą gÃļrmek için bir harici kÃŧtÃŧphane oluşturun", + "no_locked_photos_message": "Kilitli klasÃļrdeki fotoğraf ve videolar gizlidir; kitaplığınÄązda gezinirken veya arama yaparken gÃļrÃŧnmezler.", "no_name": "İsim yok", + "no_notifications": "Bildirim yok", + "no_people_found": "Eşleşen kişi bulunamadÄą", "no_places": "Yer yok", "no_results": "Sonuç bulunamadÄą", "no_results_description": "Eş anlamlÄą ya da daha genel anlamlÄą bir kelime deneyin", "no_shared_albums_message": "FotoğraflarÄą ve videolarÄą ağınÄązdaki kişilerle paylaşmak için bir albÃŧm oluşturun", "not_in_any_album": "Hiçbir albÃŧmde değil", + "not_selected": "Seçilmedi", "note_apply_storage_label_to_previously_uploaded assets": "Not: Daha Ãļnce yÃŧklenen varlÄąklar için bir depolama yolu etiketi uygulamak Ãŧzere şunu başlatÄąn", "notes": "Notlar", + "nothing_here_yet": "Burada henÃŧz bir şey yok", "notification_permission_dialog_content": "Bildirimleri etkinleştirmek için cihaz ayarlarÄąna gidin ve izin verin.", "notification_permission_list_tile_content": "Bildirimleri etkinleştirmek için izin verin.", "notification_permission_list_tile_enable_button": "Bildirimleri Etkinleştir", @@ -1235,17 +1308,22 @@ "notification_toggle_setting_description": "E-posta bildirimlerine izin ver", "notifications": "Bildirimler", "notifications_setting_description": "Bildirimleri yÃļnetin", + "oauth": "OAuth", "official_immich_resources": "Resmi Immich KaynaklarÄą", "offline": "Çevrim dÄąÅŸÄą", "ok": "Tamam", "oldest_first": "Eski olan Ãļnce", "on_this_device": "Bu cihazda", "onboarding": "Uyum SÃŧreci", - "onboarding_privacy_description": "Şu (isteğe bağlÄą) Ãļzellikler harici hizmetlere dayanÄąr ve yÃļnetim ayarlarÄąndan herhangi bir zamanda devre dÄąÅŸÄą bÄąrakÄąlabilir.", + "onboarding_locale_description": "Tercih ettiğiniz dili seçin. Bu ayarÄą daha sonra değiştirebilirsiniz.", + "onboarding_privacy_description": "Şu (isteğe bağlÄą) Ãļzellikler harici hizmetlere dayanÄąr ve ayarlardan herhangi bir zamanda devre dÄąÅŸÄą bÄąrakÄąlabilir.", + "onboarding_server_welcome_description": "Örneğinizi bazÄą yaygÄąn ayarlarla ayarlayalÄąm.", "onboarding_theme_description": "İnstance’ınÄąz için bir renk temasÄą seçin. Bunu daha sonra ayarlarÄąnÄązdan değiştirebilirsiniz.", + "onboarding_user_welcome_description": "Haydi başlayalÄąm!", "onboarding_welcome_user": "Hoş geldin, {user}", "online": "Çevrimiçi", - "only_favorites": "Sadece favoriler", + "only_favorites": "Sadece gÃļzdeler", + "open": "Aç", "open_in_map_view": "Harita gÃļrÃŧnÃŧmÃŧnde aç", "open_in_openstreetmap": "OpenStreetMap'te Aç", "open_the_search_filters": "Arama filtrelerini aç", @@ -1268,7 +1346,7 @@ "partner_page_no_more_users": "Eklenecek başka kullanÄącÄą yok", "partner_page_partner_add_failed": "Partner eklenemedi", "partner_page_select_partner": "Partner seç", - "partner_page_shared_to_title": "PaylaÅŸÄąldÄą:", + "partner_page_shared_to_title": "PaylaÅŸÄąldÄą", "partner_page_stop_sharing_content": "{partner} artÄąk fotoğraflarÄąnÄąza erişemeyecek.", "partner_sharing": "Ortak paylaÅŸÄąmÄą", "partners": "Ortaklar", @@ -1298,15 +1376,18 @@ "permanently_delete_assets_prompt": "Bu {count, plural, one {dosyayÄą} other {# dosyalarÄą}} kalÄącÄą olarak silmek istediğinizden emin misiniz? Bu işlem {count, plural, one {bu dosyayÄą} other {bu dosyalarÄą}} albÃŧmlerinizden de kaldÄąrÄąr.", "permanently_deleted_asset": "KalÄącÄą olarak silinmiş Ãļgeler", "permanently_deleted_assets_count": "{count, plural, one {# dosya} other {# dosya}} kalÄącÄą olarak silindi", + "permission": "İzin", + "permission_empty": "İzniniz boş olmamalÄą", "permission_onboarding_back": "Geri", "permission_onboarding_continue_anyway": "Yine de devam et", "permission_onboarding_get_started": "Haydi başlayalÄąm", "permission_onboarding_go_to_settings": "Ayarlara git", "permission_onboarding_permission_denied": "İzin reddedildi. Immich'i kullanmak için Ayarlar'da fotoğraf ve video izinlerini verin.", - "permission_onboarding_permission_granted": "İzin verildi. ArtÄąk hazÄąrsÄąnÄąz!", + "permission_onboarding_permission_granted": "İzin verildi! ArtÄąk hazÄąrsÄąnÄąz.", "permission_onboarding_permission_limited": "SÄąnÄąrlÄą izin. Immich'in tÃŧm fotoğrav ve videolarÄąnÄązÄą yedeklemesine ve yÃļnetmesine izin vermek için Ayarlar'da fotoğraf ve video izinlerini verin.", "permission_onboarding_request": "Immich'in fotoğraflarÄąnÄązÄą ve videolarÄąnÄązÄą gÃļrÃŧntÃŧleyebilmesi için izne ihtiyacÄą var.", "person": "Kişi", + "person_birthdate": "{date} tarihinde doğdu", "person_hidden": "{name}{hidden, select, true { (gizli)} other {}}", "photo_shared_all_users": "FotoğraflarÄąnÄązÄą tÃŧm kullanÄącÄąlarla paylaştÄąnÄąz gibi gÃļrÃŧnÃŧyor veya paylaşacak kullanÄącÄą bulunmuyor.", "photos": "Fotoğraflar", @@ -1317,6 +1398,7 @@ "pin_code_changed_successfully": "PIN kodu başarÄąyla değiştirildi", "pin_code_reset_successfully": "PIN kodu başarÄąyla sÄąfÄąrlandÄą", "pin_code_setup_successfully": "PIN kodu başarÄąyla ayarlandÄą", + "pin_verification": "PIN kodu doğrulama", "place": "Konum", "places": "Konumlar", "places_count": "{count, plural, one {{count, number} yer} other {{count, number} yer}}", @@ -1324,19 +1406,26 @@ "play_memories": "AnÄąlarÄą oynat", "play_motion_photo": "Hareketli fotoğrafÄą oynat", "play_or_pause_video": "Videoyu oynat ya da durdur", + "please_auth_to_access": "Erişim için lÃŧtfen kimliğinizi doğrulayÄąn", + "port": "Port", "preferences_settings_subtitle": "Uygulama tercihlerini dÃŧzenle", "preferences_settings_title": "Tercihler", "preset": "Ön ayar", "preview": "Önizleme", "previous": "Önceki", "previous_memory": "Önceki anÄą", - "previous_or_next_photo": "Önceki ya da sonraki fotoğraf", + "previous_or_next_day": "GÃŧn ileri/geri", + "previous_or_next_month": "Ay ileri/geri", + "previous_or_next_photo": "Fotoğraf ileri/geri", + "previous_or_next_year": "YÄąl ileri/geri", "primary": "Birincil", "privacy": "Gizlilik", + "profile": "Profil", "profile_drawer_app_logs": "GÃŧnlÃŧkler", "profile_drawer_client_out_of_date_major": "Mobil uygulama gÃŧncel değil. LÃŧtfen en son ana sÃŧrÃŧme gÃŧncelleyin.", "profile_drawer_client_out_of_date_minor": "Mobil uygulama gÃŧncel değil. LÃŧtfen en son sÃŧrÃŧme gÃŧncelleyin.", "profile_drawer_client_server_up_to_date": "Uygulama ve sunucu gÃŧncel", + "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Sunucu gÃŧncel değil. LÃŧtfen en son ana sÃŧrÃŧme gÃŧncelleyin.", "profile_drawer_server_out_of_date_minor": "Sunucu gÃŧncel değil. LÃŧtfen en son sÃŧrÃŧme gÃŧncelleyin.", "profile_image_of_user": "{user} kullanÄącÄąsÄąnÄąn profil resmi", @@ -1363,7 +1452,7 @@ "purchase_lifetime_description": "ÖmÃŧr boyu geçerli", "purchase_option_title": "SATIN ALMA SEÇENEKLERİ", "purchase_panel_info_1": "Immich'in gelişimi zaman ve çaba gerektiriyor ve tam zamanlÄą geliştiricilerimiz var. AmacÄąmÄąz, aÃ§Äąk kaynak yazÄąlÄąmÄą sÃŧrdÃŧrÃŧlebilir bir gelir kaynağı haline getirmek.", - "purchase_panel_info_2": "Bu satÄąn alma işlemi Immich'te ek işlevsellik açmayacak. Immich'in gelişimini desteklemek için size gÃŧveniyoruz.", + "purchase_panel_info_2": "Ücretli Ãļzellikler (paywall) eklememeye kararlÄą olduğumuz için, bu satÄąn alma işlemi Immich'te ek işlevsellik sağlamaz. Immich'in sÃŧrekli gelişimini desteklemek için sizin gibi kullanÄącÄąlara gÃŧveniyoruz.", "purchase_panel_title": "Projeyi destekleyin", "purchase_per_server": "Sunucu baÅŸÄąna", "purchase_per_user": "KullanÄącÄą baÅŸÄąna", @@ -1390,6 +1479,8 @@ "recent_searches": "Son aramalar", "recently_added": "Son eklenenler", "recently_added_page_title": "Son Eklenenler", + "recently_taken": "Son çekilenler", + "recently_taken_page_title": "Son Çekilenler", "refresh": "Yenile", "refresh_encoded_videos": "KodlanmÄąÅŸ videolarÄą yenile", "refresh_faces": "YÃŧzleri yenile", @@ -1408,14 +1499,21 @@ "remove_custom_date_range": "Özel tarih aralığınÄą kaldÄąr", "remove_deleted_assets": "ÇevrimdÄąÅŸÄą dosyalarÄą kaldÄąr", "remove_from_album": "AlbÃŧmden Ã§Äąkar", - "remove_from_favorites": "Favorilerden Ã§Äąkar", + "remove_from_album_action_prompt": "{count} albÃŧmden kaldÄąrÄąldÄą", + "remove_from_favorites": "GÃļzdelerden Ã§Äąkar", + "remove_from_lock_folder_action_prompt": "{count} kilitli klasÃļrden kaldÄąrÄąldÄą", + "remove_from_locked_folder": "Kilitli klasÃļrden kaldÄąr", + "remove_from_locked_folder_confirmation": "Bu fotoğraf ve videolarÄą kilitli klasÃļrden Ã§Äąkarmak istediğinizden emin misiniz? Ã‡ÄąkarÄąldÄąklarÄąnda kitaplığınÄązda gÃļrÃŧnÃŧr olacaklar.", "remove_from_shared_link": "PaylaÅŸÄąlan bağlantÄądan Ã§Äąkar", + "remove_memory": "AnÄąyÄą kaldÄąr", + "remove_photo_from_memory": "Bu anÄądan fotoğrafÄą kaldÄąr", + "remove_tag": "Etiketi kaldÄąr", "remove_url": "BağlantÄąyÄą kaldÄąr", "remove_user": "KullanÄącÄąyÄą Ã§Äąkar", "removed_api_key": "API anahtarÄą {name} kaldÄąrÄąldÄą", "removed_from_archive": "Arşivden Ã§ÄąkarÄąldÄą", - "removed_from_favorites": "Favorilerden kaldÄąrÄąldÄą", - "removed_from_favorites_count": "{count, plural, other {#}} favorilerden Ã§ÄąkarÄąldÄą", + "removed_from_favorites": "GÃļzdelerden kaldÄąrÄąldÄą", + "removed_from_favorites_count": "{count, plural, other {#}} gÃļzdelerden Ã§ÄąkarÄąldÄą", "removed_memory": "AnÄą kaldÄąrÄąldÄą", "removed_photo_from_memory": "Fotoğraf anÄądan kaldÄąrÄąldÄą", "removed_tagged_assets": "{count, plural, one {# dosya} other {# dosya}} etiketleri kaldÄąrÄąldÄą", @@ -1460,7 +1558,7 @@ "search_by_context": "Bağlama gÃļre ara", "search_by_description": "AÃ§Äąklamaya gÃļre ara", "search_by_description_example": "Sapa'da yÃŧrÃŧyÃŧş gÃŧnÃŧ", - "search_by_filename": "Dosya adÄąna gÃļre ara", + "search_by_filename": "Dosya adÄąna veya uzantÄąsÄąna gÃļre ara", "search_by_filename_example": "Örn. IMG_1234.JPG veya PNG", "search_camera_make": "Kamera markasÄąna gÃļre ara...", "search_camera_model": "Kamera modeline gÃļre ara...", @@ -1473,6 +1571,7 @@ "search_filter_date_title": "Tarih aralığı seç", "search_filter_display_option_not_in_album": "AlbÃŧmde değil", "search_filter_display_options": "GÃļrÃŧntÃŧ Seçenekleri", + "search_filter_filename": "Dosya adÄąna gÃļre ara", "search_filter_location": "Konum", "search_filter_location_title": "Konum seç", "search_filter_media_type": "Medya TÃŧrÃŧ", @@ -1480,8 +1579,10 @@ "search_filter_people_title": "Kişi seç", "search_for": "AraştÄąr", "search_for_existing_person": "Mevcut bir kişiyi ara", + "search_no_more_result": "Daha fazla sonuç yok", "search_no_people": "Kişi yok", "search_no_people_named": "\"{name}\" isimli bir kişi yok", + "search_no_result": "Sonuç bulunamadÄą. FarklÄą bir arama terimi veya kombinasyon deneyin", "search_options": "Arama seçenekleri", "search_page_categories": "Kategoriler", "search_page_motion_photos": "CanlÄą Fotoğraflar", @@ -1509,9 +1610,11 @@ "searching_locales": "Yerleri arÄąyor...", "second": "Saniye", "see_all_people": "TÃŧm kişileri gÃļr", + "select": "Seç", "select_album_cover": "AlbÃŧm kapağı seç", "select_all": "TÃŧmÃŧnÃŧ seç", "select_all_duplicates": "TÃŧm çiftleri seç", + "select_all_in": "{group} içindekilerin tÃŧmÃŧnÃŧ seç", "select_avatar_color": "Avatar rengini seç", "select_face": "YÃŧzÃŧ seç", "select_featured_photo": "Öne Ã§Äąkan fotoğrafÄą seç", @@ -1519,6 +1622,7 @@ "select_keep_all": "Hepsini sakla", "select_library_owner": "KÃŧtÃŧphane sahibini seç", "select_new_face": "Yeni yÃŧz seç", + "select_person_to_tag": "Etiketlemek için bir kişi seçin", "select_photos": "FotoğraflarÄą seç", "select_trash_all": "Hepsini çÃļpe at", "select_user_for_sharing_page_err_album": "AlbÃŧm oluşturulamadÄą", @@ -1531,6 +1635,7 @@ "server_info_box_server_url": "Sunucu URL", "server_offline": "Sunucu çevrimdÄąÅŸÄą", "server_online": "Sunucu çevrimiçi", + "server_privacy": "Sunucu Gizliliği", "server_stats": "Sunucu istatistikleri", "server_version": "Sunucu versiyonu", "set": "Ayarla", @@ -1540,6 +1645,7 @@ "set_date_of_birth": "Doğum tarihini ayarla", "set_profile_picture": "Profil resmini ayarla", "set_slideshow_to_fullscreen": "Slayt gÃļsterisini tam ekran yap", + "set_stack_primary_asset": "Birincil varlÄąk olarak ayarla", "setting_image_viewer_help": "GÃļrÃŧntÃŧleyici Ãļnce kÃŧçÃŧk resmi gÃļsterir, ardÄąndan orta boy Ãļnizlemeyi (etkinleştirilmişse) ve son olarak orijinali (etkinleştirilmişse) gÃļsterir.", "setting_image_viewer_original_subtitle": "Orijinal tam çÃļzÃŧnÃŧrlÃŧklÃŧ gÃļrÃŧntÃŧyÃŧ gÃļstermek için etkinleştirin. Veri kullanÄąmÄąnÄą azaltmak için devre dÄąÅŸÄą bÄąrakÄąn (hem ağ hem de cihaz Ãļnbelleği).", "setting_image_viewer_original_title": "Orijinal gÃļrÃŧntÃŧyÃŧ gÃļster", @@ -1560,6 +1666,8 @@ "setting_notifications_total_progress_subtitle": "Toplam yÃŧkleme ilerlemesi (tamamlanan/toplam)", "setting_notifications_total_progress_title": "Arkaplan yedeklemesi toplam ilerlemesini gÃļster", "setting_video_viewer_looping_title": "DÃļngÃŧ", + "setting_video_viewer_original_video_subtitle": "Sunucudan video aktarÄąlÄąrken, transcode (dÃļnÃŧştÃŧrÃŧlmÃŧş) sÃŧrÃŧm mevcut olsa bile orijinal dosya oynatÄąlÄąr. Bu durum, arabelleğe alma (buffering) sorunlarÄąna yol açabilir. Videolar yerel olarak mevcutsa, bu ayardan bağımsÄąz olarak orijinal kalitede oynatÄąlÄąr.", + "setting_video_viewer_original_video_title": "Orijinal videoyu zorla", "settings": "Ayarlar", "settings_require_restart": "Bu ayarÄą uygulamak için lÃŧtfen Immich'i yeniden başlatÄąn", "settings_saved": "Ayarlar kaydedildi", @@ -1568,6 +1676,7 @@ "share_add_photos": "Fotoğraf ekle", "share_assets_selected": "{count} seçili", "share_dialog_preparing": "HazÄąrlanÄąyor...", + "share_link": "BağlantÄąyÄą Paylaş", "shared": "PaylaÅŸÄąlan", "shared_album_activities_input_disable": "Yoruma kapalÄą", "shared_album_activity_remove_content": "Bu etkinliği silmek istiyor musunuz?", @@ -1580,6 +1689,7 @@ "shared_by_user": "{user} tarafÄąndan paylaÅŸÄąldÄą", "shared_by_you": "Senin tarafÄąndan paylaÅŸÄąldÄą", "shared_from_partner": "{partner} tarafÄąndan paylaÅŸÄąlan fotoğraflar", + "shared_intent_upload_button_progress_text": "{current} / {total} YÃŧklendi", "shared_link_app_bar_title": "PaylaÅŸÄąlan BağlantÄąlar", "shared_link_clipboard_copied_massage": "Panoya kopyalandÄą", "shared_link_clipboard_text": "BağlantÄą: {link}\nParola: {password}", @@ -1606,6 +1716,7 @@ "shared_link_expires_second": "SÃŧresi {count} saniye içinde doluyor", "shared_link_expires_seconds": "{count} sanyei içinde sÃŧresi doluyor", "shared_link_individual_shared": "Bireysel paylaÅŸÄąmlÄą", + "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "PaylaÅŸÄąlan BağlantÄąlarÄą YÃļnet", "shared_link_options": "PaylaÅŸÄąlan bağlantÄą seçenekleri", "shared_links": "PaylaÅŸÄąlan bağlantÄąlar", @@ -1672,12 +1783,14 @@ "start_date": "BaşlangÄąÃ§ tarihi", "state": "Eyalet/İl", "status": "Durum", + "stop_casting": "YansÄątmayÄą durdur", "stop_motion_photo": "Hareketli fotoğrafÄą durdur", "stop_photo_sharing": "FotoğraflarÄąnÄązÄą paylaşmayÄą durdurmak mÄą istiyorsunuz?", "stop_photo_sharing_description": "{partner} artÄąk fotoğraflarÄąnÄąza erişemeyecek.", "stop_sharing_photos_with_user": "Bu kullanÄącÄą ile fotoğraflarÄąnÄązÄą paylaşmayÄą durdurun", "storage": "Depolama alanÄą", "storage_label": "Depolama yolu", + "storage_quota": "Depolama KotasÄą", "storage_usage": "{used} / {available} kullanÄąldÄą", "submit": "GÃļnder", "suggestions": "Öneriler", @@ -1714,7 +1827,7 @@ "theme_setting_system_primary_color_title": "Sistem rengini kullan", "theme_setting_system_theme_switch": "Otomatik (sistem ayarÄąna gÃļre)", "theme_setting_theme_subtitle": "Uygulama temasÄą seç", - "theme_setting_three_stage_loading_subtitle": "Üç aşamalÄą yÃŧkleme yÃŧkleme performansÄąnÄą artÄąrabilir ancak Ãļnemli ÃļlçÃŧde daha yÃŧksek ağ yÃŧkÃŧne sebep olur.", + "theme_setting_three_stage_loading_subtitle": "Üç aşamalÄą yÃŧkleme, yÃŧkleme performansÄąnÄą artÄąrabilir ancak Ãļnemli ÃļlçÃŧde daha yÃŧksek ağ yÃŧkÃŧne sebep olur.", "theme_setting_three_stage_loading_title": "Üç aşamalÄą yÃŧklemeyi etkinleştir", "they_will_be_merged_together": "Birlikte birleştirilecekler", "third_party_resources": "ÜçÃŧncÃŧ taraf kaynaklar", @@ -1723,7 +1836,7 @@ "timezone": "Zaman dilimi", "to_archive": "Arşivle", "to_change_password": "Şifreyi değiştir", - "to_favorite": "Favorilere ekle", + "to_favorite": "GÃļzdelere ekle", "to_login": "Oturum aç", "to_parent": "Üst Ãļğeye git", "to_trash": "ÇÃļpe taÅŸÄą", @@ -1749,7 +1862,8 @@ "unable_to_setup_pin_code": "PIN kodu ayarlanamadÄą", "unarchive": "Arşivden Ã§Äąkar", "unarchived_count": "{count, plural, other {# arşivden Ã§ÄąkarÄąldÄą}}", - "unfavorite": "Favorilerden kaldÄąr", + "undo": "Geri al", + "unfavorite": "GÃļzdelerden kaldÄąr", "unhide_person": "Kişiyi gÃļster", "unknown": "Bilinmeyen", "unknown_country": "Bilinmeyen Ãŧlke", @@ -1765,9 +1879,11 @@ "unsaved_change": "Kaydedilmemiş değişiklik", "unselect_all": "TÃŧmÃŧnÃŧ seçimini kaldÄąr", "unselect_all_duplicates": "TÃŧm çiftlerin seçimini kaldÄąr", + "unselect_all_in": "{group} içindeki tÃŧm seçimleri kaldÄąr", "unstack": "YığınÄą kaldÄąr", "unstacked_assets_count": "{count, plural, one {# dosya} other {# dosya}} yığınÄą kaldÄąrÄąldÄą", "up_next": "SÄąradaki", + "updated_at": "GÃŧncellenme", "updated_password": "Şifreyi gÃŧncelle", "upload": "YÃŧkle", "upload_concurrency": "YÃŧkleme eşzamanlÄąlığı", @@ -1780,14 +1896,20 @@ "upload_status_errors": "Hatalar", "upload_status_uploaded": "YÃŧklendi", "upload_success": "YÃŧkleme başarÄąlÄą, yÃŧklenen yeni Ãļgeleri gÃļrebilmek için sayfayÄą yenileyin.", + "upload_to_immich": "Immich'e YÃŧkle ({count})", + "uploading": "YÃŧkleniyor", + "url": "URL", "usage": "KullanÄąm", + "use_biometric": "Biyometri kullan", "use_current_connection": "mevcut bağlantÄąyÄą kullan", "use_custom_date_range": "Bunun yerine Ãļzel tarih aralığınÄą kullan", "user": "KullanÄącÄą", + "user_has_been_deleted": "Bu kullanÄącÄą silindi.", "user_id": "KullanÄącÄą ID", "user_liked": "{type, select, photo {Bu fotoğraf} video {Bu video} asset {Bu dosya} other {Bu}} {user} tarafÄąndan beğenildi", "user_pin_code_settings": "PIN Kodu", "user_pin_code_settings_description": "PIN kodunuzu yÃļnetin", + "user_privacy": "KullanÄącÄą Gizliliği", "user_purchase_settings": "SatÄąn Alma", "user_purchase_settings_description": "SatÄąn alma işlemlerini yÃļnet", "user_role_set": "{user}, {role} olarak ayarlandÄą", @@ -1805,6 +1927,7 @@ "version_announcement_message": "Merhaba! Immich'in yeni bir sÃŧrÃŧmÃŧ mevcut. LÃŧtfen yapÄąlandÄąrmanÄązÄąn gÃŧncel olduğundan emin olmak için sÃŧrÃŧm notlarÄąnÄą okumak için biraz zaman ayÄąrÄąn, Ãļzellikle WatchTower veya Immich kurulumunuzu otomatik olarak gÃŧncelleyen bir mekanizma kullanÄąyorsanÄąz yanlÄąÅŸ yapÄąlandÄąrmalarÄąn ÃļnÃŧne geçmek adÄąna bu Ãļnemlidir.", "version_history": "Versiyon geçmişi", "version_history_item": "{version}, {date} tarihinde kuruldu", + "video": "Video", "video_hover_setting": "Üzerinde durulduğunda video Ãļnizlemesi oynat", "video_hover_setting_description": "Öğe Ãŧzerinde fareyle durulduğunda video kÃŧçÃŧk resmini oynatÄąr. Bu Ãļzellik devre dÄąÅŸÄąyken, oynatma simgesine fareyle gidilerek oynatma başlatÄąlabilir.", "videos": "Videolar", @@ -1819,7 +1942,9 @@ "view_name": "GÃļster", "view_next_asset": "Sonraki dosyayÄą gÃļrÃŧntÃŧle", "view_previous_asset": "Önceki dosyayÄą gÃļrÃŧntÃŧle", + "view_qr_code": "QR kodu gÃļrÃŧntÃŧle", "view_stack": "YığınÄą gÃļrÃŧntÃŧle", + "view_user": "KullanÄącÄąyÄą GÃļrÃŧntÃŧle", "viewer_remove_from_stack": "Yığından KaldÄąr", "viewer_stack_use_as_main_asset": "Ana fotoğraf olarak kullan", "viewer_unstack": "YığınÄą KaldÄąr", @@ -1830,6 +1955,7 @@ "welcome": "Hoş geldiniz", "welcome_to_immich": "Immich'e hoş geldiniz", "wifi_name": "Wi-Fi AdÄą", + "wrong_pin_code": "YanlÄąÅŸ PIN kodu", "year": "YÄąl", "years_ago": "{years, plural, one {bir yÄąl} other {# yÄąl}} Ãļnce", "yes": "Evet", diff --git a/i18n/uk.json b/i18n/uk.json index 7b843ceff9..8a375e627a 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -244,6 +244,7 @@ "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": "КĐĩŅ€ŅƒĐšŅ‚Đĩ ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€ĐžŅŽ Ņ‚ĐĩĐē Ņ‚Đ° Ņ–ĐŧĐĩĐŊĐĩĐŧ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐžĐŗĐž Ņ„Đ°ĐšĐģ҃", @@ -463,7 +464,6 @@ "assets": "ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", "assets_added_count": "ДодаĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", "assets_added_to_album_count": "ДодаĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}} Đ´Đž аĐģŅŒĐąĐžĐŧ҃", - "assets_added_to_name_count": "ДодаĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} Đ´Đž {hasName, select, true {{name}} other {ĐŊĐžĐ˛ĐžĐŗĐž аĐģŅŒĐąĐžĐŧ҃}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Đ ĐĩŅŅƒŅ€Ņ} other {Đ ĐĩŅŅƒŅ€ŅĐ¸}} ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", "assets_count": "{count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", "assets_deleted_permanently": "{count} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚(и) ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž", @@ -702,7 +702,6 @@ "daily_title_text_date": "Е, МММ Đ´Đ´", "daily_title_text_date_year": "Е, МММ Đ´Đ´, ҀҀҀҀ", "dark": "ĐĸĐĩĐŧĐŊиК", - "darkTheme": "ПĐĩŅ€ĐĩĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ‚ĐĩĐŧĐŊ҃ Ņ‚ĐĩĐŧ҃", "date_after": "Đ”Đ°Ņ‚Đ° ĐŋҖҁĐģŅ", "date_and_time": "Đ”Đ°Ņ‚Đ° Ņ– Ņ‡Đ°Ņ", "date_before": "Đ”Đ°Ņ‚Đ° Đ´Đž", diff --git a/i18n/vi.json b/i18n/vi.json index 5890ef87db..10c883dc70 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -461,7 +461,6 @@ "assets": "CÃĄc táē­p tin", "assets_added_count": "ÄÃŖ thÃĒm {count, plural, one {# máģĨc} other {# máģĨc}}", "assets_added_to_album_count": "ÄÃŖ thÃĒm {count, plural, one {# máģĨc} other {# máģĨc}} vào album", - "assets_added_to_name_count": "ÄÃŖ thÃĒm {count, plural, one {# máģĨc} other {# máģĨc}} vào {hasName, select, true {{name}} other {album máģ›i}}", "assets_count": "{count, plural, one {# máģĨc} other {# máģĨc}}", "assets_deleted_permanently": "ÄÃŖ xoÃĄ vÄŠnh viáģ…n {count} máģĨc", "assets_deleted_permanently_from_server": "ÄÃŖ xoÃĄ vÄŠnh viáģ…n {count} máģĨc kháģi mÃĄy cháģ§ Immich", @@ -534,7 +533,7 @@ "backup_controller_page_start_backup": "Báē¯t đáē§u sao lưu", "backup_controller_page_status_off": "Sao lưu táģą Ä‘áģ™ng khi áģŠng dáģĨng hoáēĄt đáģ™ng đang táē¯t", "backup_controller_page_status_on": "Sao lưu táģą Ä‘áģ™ng khi áģŠng dáģĨng hoáēĄt đáģ™ng đang báē­t", - "backup_controller_page_storage_format": "ÄÃŖ sáģ­ dáģĨng {used} cáģ§a {total}", + "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 áģŠng dáģĨng hoáēĄt đáģ™ng", @@ -698,7 +697,6 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Táģ‘i", - "darkTheme": "Chuyáģƒn đáģ•i cháģ§ Ä‘áģ táģ‘i", "date_after": "Ngày sau", "date_and_time": "Ngày và giáģ", "date_before": "Ngày trưáģ›c", @@ -1016,7 +1014,7 @@ "group_year": "NhÃŗm theo năm", "haptic_feedback_switch": "Báē­t pháēŖn háģ“i haptic", "haptic_feedback_title": "PháēŖn háģ“i Hapic", - "has_quota": "CÃŗ háēĄn máģŠc", + "has_quota": "HáēĄn máģŠc", "header_settings_add_header_tip": "ThÃĒm Header", "header_settings_field_validator_msg": "Trưáģng này không đưáģŖc đáģƒ tráģ‘ng", "header_settings_header_name_input": "TÃĒn Header", @@ -1426,7 +1424,7 @@ "purchase_button_activate": "Kích hoáēĄt", "purchase_button_buy": "Mua", "purchase_button_buy_immich": "Mua Immich", - "purchase_button_never_show_again": "Không hiáģƒn tháģ‹ láēĄi", + "purchase_button_never_show_again": "Không hiáģ‡n láēĄi", "purchase_button_reminder": "Nháē¯c tôi trong 30 ngày", "purchase_button_remove_key": "XÃŗa khÃŗa", "purchase_button_select": "Cháģn", @@ -1504,7 +1502,7 @@ "rename": "Đáģ•i tÃĒn", "repair": "Sáģ­a cháģ¯a", "repair_no_results_message": "CÃĄc táē­p tin không đưáģŖc theo dÃĩi và báģ‹ máēĨt sáēŊ xuáēĨt hiáģ‡n áģŸ Ä‘Ãĸy", - "replace_with_upload": "Thay tháēŋ báēąng táē­p tin táēŖi lÃĒn", + "replace_with_upload": "Thay tháēŋ táē­p tin khÃĄc", "repository": "Kho lưu tráģ¯", "require_password": "YÃĒu cáē§u máē­t kháēŠu", "require_user_to_change_password_on_first_login": "YÃĒu cáē§u ngưáģi dÚng thay đáģ•i máē­t kháēŠu áģŸ láē§n đáē§u đăng nháē­p", @@ -1522,7 +1520,7 @@ "restored_asset": "áēĸnh Ä‘ÃŖ đưáģŖc khôi pháģĨc", "resume": "Tiáēŋp táģĨc", "retry_upload": "Tháģ­ táēŖi lÃĒn láēĄi", - "review_duplicates": "Xem xÊt cÃĄc máģĨc trÚng láēˇp", + "review_duplicates": "Xem láēĄi cÃĄc máģĨc trÚng láēˇp", "role": "Vai trÃ˛", "role_editor": "Ngưáģi cháģ‰nh sáģ­a", "role_viewer": "Ngưáģi xem", @@ -1617,7 +1615,7 @@ "server_info_box_app_version": "PhiÃĒn báēŖn áģŠng dáģĨng", "server_info_box_server_url": "Đáģ‹a cháģ‰ mÃĄy cháģ§", "server_offline": "MÃĄy cháģ§ ngoáēĄi tuyáēŋn", - "server_online": "MÃĄy cháģ§ tráģąc tuyáēŋn", + "server_online": "PhiÃĒn báēŖn", "server_privacy": "Quyáģn riÃĒng tư mÃĄy cháģ§", "server_stats": "Tháģ‘ng kÃĒ mÃĄy cháģ§", "server_version": "PhiÃĒn báēŖn mÃĄy cháģ§", @@ -1772,7 +1770,7 @@ "storage": "Báģ™ nháģ›", "storage_label": "NhÃŖn lưu tráģ¯", "storage_quota": "Giáģ›i háēĄn Dung lưáģŖng", - "storage_usage": "ÄÃŖ sáģ­ dáģĨng {used} cáģ§a {available}", + "storage_usage": "ÄÃŖ dÚng {used} cáģ§a {available}", "submit": "Gáģ­i", "suggestions": "GáģŖi ÃŊ", "sunrise_on_the_beach": "BÃŦnh minh trÃĒn bÃŖi biáģƒn", @@ -1819,7 +1817,7 @@ "to_change_password": "Đáģ•i máē­t kháēŠu", "to_favorite": "YÃĒu thích", "to_login": "Đăng nháē­p", - "to_parent": "Đi táģ›i thư máģĨc cha", + "to_parent": "Váģ thư máģĨc gáģ‘c", "to_trash": "XÃŗa", "toggle_settings": "Chuyáģƒn đáģ•i cài đáēˇt", "total": "Táģ•ng cáģ™ng", @@ -1894,7 +1892,7 @@ "user_purchase_settings_description": "QuáēŖn lÃŊ máģĨc mua cáģ§a báēĄn", "user_role_set": "Đáēˇt {user} làm {role}", "user_usage_detail": "Chi tiáēŋt sáģ­ dáģĨng cáģ§a ngưáģi dÚng", - "user_usage_stats": "Tháģ‘ng kÃĒ sáģ­ dáģĨng cáģ§a tài khoáēŖn", + "user_usage_stats": "Tháģ‘ng kÃĒ sáģ­ dáģĨng tài khoáēŖn", "user_usage_stats_description": "Xem tháģ‘ng kÃĒ sáģ­ dáģĨng cáģ§a tài khoáēŖn", "username": "TÃĒn ngưáģi dÚng", "users": "Ngưáģi dÚng", diff --git a/i18n/zh_Hant.json b/i18n/zh_Hant.json index e5a8a65e25..334da7bfb1 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.json @@ -464,7 +464,6 @@ "assets": "é …į›Ž", "assets_added_count": "åˇ˛æ–°åĸž {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", "assets_added_to_album_count": "厞將 {count, plural, other {# å€‹é …į›Ž}}加å…Ĩᛏį°ŋ", - "assets_added_to_name_count": "厞將 {count, plural, other {# å€‹é …į›Ž}}加å…Ĩ{hasName, select, true {{name}} other {æ–°į›¸į°ŋ}}", "assets_cannot_be_added_to_album_count": "{count. plural, one {個} other {個}} é …į›ŽæœĒčƒŊčĸĢæˇģåŠ č‡ŗį›¸į°ŋ", "assets_count": "{count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", "assets_deleted_permanently": "{count} å€‹é …į›Žåˇ˛čĸĢæ°¸äš…åˆĒ除", @@ -699,7 +698,6 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "YYYYåš´M月Dæ—Ĩ (E)", "dark": "æˇąč‰˛", - "darkTheme": "åˆ‡æ›įˆ˛æˇąč‰˛ä¸ģ題", "date_after": "æ—Ĩ期䚋垌", "date_and_time": "æ—ĨæœŸčˆ‡æ™‚é–“", "date_before": "æ—Ĩ期䚋前", diff --git a/i18n/zh_SIMPLIFIED.json b/i18n/zh_SIMPLIFIED.json index 17c6954ab7..bcbdcc5d6b 100644 --- a/i18n/zh_SIMPLIFIED.json +++ b/i18n/zh_SIMPLIFIED.json @@ -166,6 +166,20 @@ "metadata_settings_description": "įŽĄį†å…ƒæ•°æŽčŽžįŊŽ", "migration_job": "čŋį§ģ", "migration_job_description": "å°†éĄšį›Žå’Œäēē脏蝆åˆĢįš„įŧŠį•Ĩ回čŋį§ģåˆ°æœ€æ–°įš„æ–‡äģļ多į쓿ž„", + "nightly_tasks_cluster_faces_setting_description": "å¯šæ–°æŖ€æĩ‹åˆ°įš„éĸ部čŋ›čĄŒéĸéƒ¨č¯†åˆĢ", + "nightly_tasks_cluster_new_faces_setting": "įž¤į섿–°éĸ孔", + "nightly_tasks_database_cleanup_setting": "数捎å瓿¸…ᐆäģģåŠĄ", + "nightly_tasks_database_cleanup_setting_description": "æ¸…į†æ•°æŽåē“中čŋ‡æœŸįš„æ—§æ•°æŽ", + "nightly_tasks_generate_memories_setting": "į”Ÿæˆå›žåŋ†", + "nightly_tasks_generate_memories_setting_description": "äģŽéĄšį›Žä¸­į”Ÿæˆæ–°įš„回åŋ†", + "nightly_tasks_missing_thumbnails_setting": "į”Ÿæˆįŧēå¤ąįš„įŧŠį•Ĩ回", + "nightly_tasks_missing_thumbnails_setting_description": "ä¸ēį”ŸæˆįŧŠį•Ĩ回队列无įŧŠį•Ĩå›žįš„éĄšį›Ž", + "nightly_tasks_settings": "夜间äģģåŠĄčŽžįŊŽ", + "nightly_tasks_settings_description": "įŽĄį†å¤œé—´äģģåŠĄ", + "nightly_tasks_start_time_setting": "åŧ€å§‹æ—ļ间", + "nightly_tasks_start_time_setting_description": "æœåŠĄå™¨åŧ€å§‹čŋčĄŒå¤œé—´äģģåŠĄįš„æ—ļ间", + "nightly_tasks_sync_quota_usage_setting": "同æ­Ĩ配éĸäŊŋį”¨æƒ…å†ĩ", + "nightly_tasks_sync_quota_usage_setting_description": "栚捎åŊ“前äŊŋį”¨æƒ…å†ĩæ›´æ–°į”¨æˆˇå­˜å‚¨é…éĸ", "no_paths_added": "æ— åˇ˛æˇģåŠ čˇ¯åž„", "no_pattern_added": "æ— åˇ˛æˇģåŠ č§„åˆ™", "note_apply_storage_label_previous_assets": "提į¤ēīŧščĻå°†å­˜å‚¨æ ‡į­žåē”ᔍäēŽäš‹å‰ä¸Šäŧ įš„éĄšį›ŽīŧŒéœ€čρčŋčĄŒ", @@ -196,6 +210,8 @@ "oauth_mobile_redirect_uri": "į§ģ动įĢ¯é‡åŽšå‘ URI", "oauth_mobile_redirect_uri_override": "į§ģ动įĢ¯é‡åŽšå‘ URI čφᛖ", "oauth_mobile_redirect_uri_override_description": "åŊ“ OAuth æäž›å•†ä¸å…čŽ¸äŊŋᔍį§ģ动 URI æ—ļ吝ᔍīŧŒåĻ‚â€œ''{callback}''”", + "oauth_role_claim": "č§’č‰˛åŖ°æ˜Ž", + "oauth_role_claim_description": "æ šæŽæ­¤åŖ°æ˜Žįš„å­˜åœ¨č‡Ē动授äēˆįŽĄį†å‘˜čŽŋé—Žæƒé™ã€‚åŖ°æ˜Žå¯äģĨ是“user”īŧˆį”¨æˆˇīŧ‰æˆ–“admin”īŧˆįŽĄį†å‘˜īŧ‰ã€‚", "oauth_settings": "OAuth", "oauth_settings_description": "įŽĄį† OAuth į™ģåŊ•莞įŊŽ", "oauth_settings_more_details": "å…ŗäēŽæ­¤åŠŸčƒŊįš„æ›´å¤šč¯Ļįģ†äŋĄæ¯īŧŒč¯ˇæŸĨįœ‹į›¸å…ŗæ–‡æĄŖã€‚", @@ -357,6 +373,8 @@ "admin_password": "įŽĄį†å‘˜å¯†į ", "administration": "įŗģįģŸįŽĄį†", "advanced": "é̘įē§", + "advanced_settings_beta_timeline_subtitle": "äŊ“éĒŒå…¨æ–°įš„åē”ᔍፋåē", + "advanced_settings_beta_timeline_title": "æĩ‹č¯•į‰ˆæ—ļ间įēŋ", "advanced_settings_enable_alternate_media_filter_subtitle": "äŊŋį”¨æ­¤é€‰éĄšå¯åœ¨åŒæ­Ĩčŋ‡į¨‹ä¸­æ šæŽå¤‡į”¨æĄäģļį­›é€‰éĄšį›Žã€‚äģ…åŊ“您在åē”ᔍፋåēæŖ€æĩ‹æ‰€æœ‰į›¸å†Œå‡é‡åˆ°é—Žéĸ˜æ—￉å°č¯•此功čƒŊ。", "advanced_settings_enable_alternate_media_filter_title": "[厞énj] äŊŋį”¨å¤‡į”¨įš„čŽžå¤‡į›¸å†ŒåŒæ­Ĩį­›é€‰æĄäģļ", "advanced_settings_log_level_title": "æ—Ĩåŋ—į­‰įē§: {level}", @@ -388,6 +406,7 @@ "album_options": "į›¸å†ŒčŽžįŊŽ", "album_remove_user": "į§ģé™¤į”¨æˆˇīŧŸ", "album_remove_user_confirmation": "įĄŽåŽščρį§ģ除“{user}”吗īŧŸ", + "album_search_not_found": "æœĒ扞到įŦĻ合搜į´ĸæĄäģļįš„į›¸å†Œ", "album_share_no_users": "įœ‹čĩˇæĨæ‚¨åˇ˛ä¸Žæ‰€æœ‰į”¨æˆˇå…ąäēĢä熿­¤į›¸å†ŒīŧŒæˆ–č€…æ‚¨æ šæœŦæ˛Ąæœ‰äģģäŊ•į”¨æˆˇå¯å…ąäēĢ。", "album_updated": "į›¸å†Œæœ‰æ›´æ–°", "album_updated_setting_description": "åŊ“å…ąäēĢį›¸å†Œæœ‰æ–°éĄšį›Žæ—ļæŽĨæ”ļ邮äģļ通įŸĨ", @@ -407,6 +426,7 @@ "albums_default_sort_order": "éģ˜čŽ¤į›¸å†ŒæŽ’åēæ–šåŧ", "albums_default_sort_order_description": "创åģēæ–°į›¸å†Œæ—ļįš„éĄšį›Žåˆå§‹æŽ’åēæ–šåŧã€‚", "albums_feature_description": "可与å…ļäģ–į”¨æˆˇå…ąäēĢįš„éĄšį›Žæ”ļč—ã€‚", + "albums_on_device_count": "čŽžå¤‡ä¸Šįš„į›¸å†Œīŧˆ{count} ä¸Ēīŧ‰", "all": "全部", "all_albums": "æ‰€æœ‰į›¸å†Œ", "all_people": "全部äēēį‰Š", @@ -427,12 +447,13 @@ "app_settings": "åē”į”¨čŽžįŊŽ", "appears_in": "å‡ēįŽ°äēŽ", "archive": "åŊ’æĄŖ", + "archive_action_prompt": "厞将 {count} 饚æˇģ加到åŊ’æĄŖ", "archive_or_unarchive_photo": "åŊ’æĄŖæˆ–å–æļˆåŊ’æĄŖį…§į‰‡", "archive_page_no_archived_assets": "æœĒ扞到åŊ’æĄŖéĄšį›Ž", "archive_page_title": "åŊ’æĄŖīŧˆ{count}īŧ‰", "archive_size": "åŊ’æĄŖå¤§å°", "archive_size_description": "配įŊŽä¸‹čŊŊåŊ’æĄŖå¤§å°īŧˆGBīŧ‰", - "archived": "åˇ˛å­˜æĄŖ", + "archived": "厞åŊ’æĄŖ", "archived_count": "{count, plural, other {厞åŊ’æĄŖ # 饚}}", "are_these_the_same_person": "äģ–äģŦ是同一äŊäēē吗īŧŸ", "are_you_sure_to_do_this": "įĄŽåŽščρčŋ™æ ˇåšå—īŧŸ", @@ -464,7 +485,6 @@ "assets": "éĄšį›Ž", "assets_added_count": "厞æˇģ加{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}", "assets_added_to_album_count": "厞æˇģ加{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}åˆ°į›¸å†Œ", - "assets_added_to_name_count": "厞æˇģ加{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}到{hasName, select, true {{name}} other {æ–°į›¸å†Œ}}", "assets_cannot_be_added_to_album_count": "æ— æŗ•æˇģ加 {count, plural, one {ä¸ĒéĄšį›Ž} other {ä¸ĒéĄšį›Ž}} åˆ°į›¸å†Œä¸­", "assets_count": "{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}", "assets_deleted_permanently": "{count} ä¸ĒéĄšį›Žåˇ˛čĸĢæ°¸äš…删除", @@ -553,6 +573,8 @@ "backup_options_page_title": "备äģŊ选项", "backup_setting_subtitle": "įŽĄį†åŽå°å’Œå‰å°ä¸Šäŧ čŽžįŊŽ", "backward": "后退", + "beta_sync": "æĩ‹č¯•į‰ˆåŒæ­ĨįŠļ态", + "beta_sync_subtitle": "įŽĄį†æ–°įš„åŒæ­ĨįŗģįģŸ", "biometric_auth_enabled": "į”Ÿį‰Šč¯†åˆĢčēĢäģŊéĒŒč¯åˇ˛å¯į”¨", "biometric_locked_out": "您čĸĢé”åŽšåœ¨į”Ÿį‰Šč¯†åˆĢčēĢäģŊéĒŒč¯äš‹å¤–", "biometric_no_options": "æ˛Ąæœ‰å¯į”¨įš„į”Ÿį‰Šč¯†åˆĢ选项", @@ -570,7 +592,7 @@ "cache_settings_clear_cache_button": "清除įŧ“å­˜", "cache_settings_clear_cache_button_title": "清除åē”ᔍįŧ“å­˜ã€‚åœ¨é‡æ–°į”Ÿæˆįŧ“存䚋前īŧŒå°†æ˜žč‘—åŊąå“åē”į”¨įš„æ€§čƒŊ。", "cache_settings_duplicated_assets_clear_button": "清除", - "cache_settings_duplicated_assets_subtitle": "厞加å…Ĩéģ‘åå•įš„į…§į‰‡å’Œč§†éĸ‘", + "cache_settings_duplicated_assets_subtitle": "åē”ᔍፋåēåŋŊį•Ĩįš„į…§į‰‡å’Œč§†éĸ‘", "cache_settings_duplicated_assets_title": "é‡å¤éĄšį›Žīŧˆ{count}īŧ‰", "cache_settings_statistics_album": "回åē“įŧŠį•Ĩ回", "cache_settings_statistics_full": "厌整回像", @@ -587,6 +609,7 @@ "cancel": "取æļˆ", "cancel_search": "取æļˆæœį´ĸ", "canceled": "åˇ˛å–æļˆ", + "canceling": "取æļˆä¸­", "cannot_merge_people": "æ— æŗ•åˆåšļäēēį‰Š", "cannot_undo_this_action": "æŗ¨æ„īŧšč¯Ĩ操äŊœæ— æŗ•čĸĢæ’¤æļˆīŧ", "cannot_update_the_description": "æ— æŗ•æ›´æ–°æčŋ°", @@ -703,7 +726,7 @@ "daily_title_text_date": "MMM dd (E)", "daily_title_text_date_year": "YYYYåš´M月Dæ—Ĩ (E)", "dark": "æˇąč‰˛", - "darkTheme": "æš—č‰˛ä¸ģéĸ˜", + "dark_theme": "切æĸæˇąč‰˛ä¸ģéĸ˜", "date_after": "åŧ€å§‹æ—Ĩ期", "date_and_time": "æ—Ĩ期与æ—ļ间", "date_before": "į쓿Ÿæ—Ĩ期", @@ -719,6 +742,7 @@ "default_locale": "éģ˜čޤ地åŒē", "default_locale_description": "æ šæŽæ‚¨įš„æĩč§ˆå™¨åœ°åŒē莞įŊŽæ—Ĩ期和数字昞į¤ēæ ŧåŧ", "delete": "删除", + "delete_action_prompt": "åˇ˛æ°¸äš…åˆ é™¤ {count} 饚", "delete_album": "åˆ é™¤į›¸å†Œ", "delete_api_key_prompt": "įĄŽåŽšåˆ é™¤æ­¤ API 密é’Ĩ吗īŧŸ", "delete_dialog_alert": "čŋ™äē›éĄšį›Žå°†äģŽ Immich å’Œæ‚¨įš„čŽžå¤‡ä¸­æ°¸äš…åˆ é™¤", @@ -732,6 +756,7 @@ "delete_key": "删除密é’Ĩ", "delete_library": "删除回åē“", "delete_link": "删除铞æŽĨ", + "delete_local_action_prompt": "åˇ˛åˆ é™¤æœŦåœ°éĄšį›Ž{count}饚", "delete_local_dialog_ok_backed_up_only": "äģ…åˆ é™¤åˇ˛å¤‡äģŊéĄšį›Ž", "delete_local_dialog_ok_force": "įĄŽčŽ¤åˆ é™¤", "delete_others": "删除å…ļ厃", @@ -745,6 +770,7 @@ "description": "描čŋ°", "description_input_hint_text": "æˇģ加描čŋ°...", "description_input_submit_error": "更新描čŋ°æ—ļå‡ē错īŧŒč¯ˇæŖ€æŸĨæ—Ĩåŋ—äģĨčŽˇå–æ›´å¤šč¯Ļįģ†äŋĄæ¯", + "deselect_all": "取æļˆå…¨é€‰", "details": "č¯Ļ情", "direction": "斚向", "disabled": "厞įρᔍ", @@ -762,6 +788,7 @@ "documentation": "å¸ŽåŠŠæ–‡æĄŖ", "done": "厌成", "download": "下čŊŊ", + "download_action_prompt": "æ­Ŗåœ¨ä¸‹čŊŊ {count} ä¸ĒéĄšį›Ž", "download_canceled": "下čŊŊåˇ˛å–æļˆ", "download_complete": "下čŊŊ厌成", "download_enqueue": "厞加å…Ĩ下čŊŊ队列", @@ -799,6 +826,7 @@ "edit_key": "įŧ–čž‘ API 密é’Ĩ", "edit_link": "įŧ–螑铞æŽĨ", "edit_location": "įŧ–čž‘äŊįŊŽ", + "edit_location_action_prompt": "{count} ä¸ĒäŊįŊŽåˇ˛įŧ–čž‘", "edit_location_dialog_title": "äŊįŊŽ", "edit_name": "įŧ–čž‘åį§°", "edit_people": "įŧ–čž‘äēēį‰Š", @@ -817,6 +845,7 @@ "empty_trash": "清įŠē回æ”ļįĢ™", "empty_trash_confirmation": "įĄŽåŽščĻæ¸…įŠē回æ”ļįĢ™īŧŸčŋ™å°†æ°¸äš…删除回æ”ļįĢ™ä¸­įš„æ‰€æœ‰éĄšį›Žã€‚\næŗ¨æ„īŧšč¯Ĩ操äŊœæ— æŗ•æ’¤æļˆīŧ", "enable": "吝ᔍ", + "enable_backup": "吝ᔍ备äģŊ", "enable_biometric_auth_description": "输å…Ĩæ‚¨įš„PIN᠁äģĨå¯į”¨į”Ÿį‰Šč¯†åˆĢčēĢäģŊénj蝁", "enabled": "厞吝ᔍ", "end_date": "į쓿Ÿæ—Ĩ期", @@ -984,6 +1013,7 @@ "failed_to_load_assets": "加čŊŊéĄšį›Žå¤ąč´Ĩ", "failed_to_load_folder": "加čŊŊ文äģļå¤šå¤ąč´Ĩ", "favorite": "æ”ļ藏", + "favorite_action_prompt": "厞将 {count} 饚æˇģ加到æ”ļ藏", "favorite_or_unfavorite_photo": "æ”ļč—æˆ–å–æļˆæ”ļč—į…§į‰‡", "favorites": "æ”ļč—å¤š", "favorites_page_no_favorites": "æœĒ扞到æ”ļč—éĄšį›Ž", @@ -1023,6 +1053,9 @@ "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": "æ ‡å¤´åį§°", @@ -1055,6 +1088,7 @@ "host": "æœåŠĄå™¨", "hour": "æ—ļ", "id": "ID", + "idle": "įŠē闲", "ignore_icloud_photos": "åŋŊį•Ĩ iCloud ᅧቇ", "ignore_icloud_photos_description": "存储在 iCloud ä¸­įš„į…§į‰‡ä¸äŧšä¸Šäŧ č‡ŗ Immich æœåŠĄå™¨", "image": "å›žį‰‡", @@ -1127,6 +1161,7 @@ "library_page_sort_created": "创åģēæ—Ĩ期", "library_page_sort_last_modified": "上æŦĄäŋŽæ”š", "library_page_sort_title": "į›¸å†Œæ ‡éĸ˜", + "licenses": "čŽ¸å¯č¯", "light": "æĩ…色", "like_deleted": "åˇ˛åˆ é™¤įš„æ”ļ藏", "link_motion_video": "链æŽĨåŠ¨æ€č§†éĸ‘", @@ -1136,7 +1171,9 @@ "list": "åˆ—čĄ¨", "loading": "加čŊŊ中", "loading_search_results_failed": "加čŊŊ搜į´ĸį쓿žœå¤ąč´Ĩ", + "local": "æœŦ地", "local_asset_cast_failed": "æ— æŗ•æŠ•æ”žæœĒ上äŧ č‡ŗæœåŠĄå™¨įš„éĄšį›Ž", + "local_assets": "æœŦåœ°éĄšį›Ž", "local_network": "æœŦ地įŊ‘įģœ", "local_network_sheet_info": "åŊ“äŊŋį”¨æŒ‡åŽšįš„ Wi-Fi įŊ‘į윿—ļīŧŒåē”ᔍፋåēå°†é€ščŋ‡æ­¤ URL čŽŋé—ŽæœåŠĄå™¨", "location_permission": "厚äŊæƒé™", @@ -1246,6 +1283,7 @@ "more": "更多", "move": "į§ģ动", "move_off_locked_folder": "į§ģå‡ē锁厚文äģļ多", + "move_to_lock_folder_action_prompt": "厞将 {count} 饚æˇģ加到锁厚文äģļ多", "move_to_locked_folder": "į§ģ动到锁厚文äģļ多", "move_to_locked_folder_confirmation": "čŋ™äē›į…§į‰‡å’Œč§†éĸ‘å°†äģŽæ‰€æœ‰į›¸å†Œä¸­į§ģ除īŧŒåĒčƒŊ在锁厚文äģļ多中æŸĨįœ‹", "moved_to_archive": "厞åŊ’æĄŖ {count, plural, one {# ä¸ĒéĄšį›Ž} other {# ä¸ĒéĄšį›Ž}}", @@ -1292,6 +1330,7 @@ "no_results": "无į쓿žœ", "no_results_description": "å°č¯•äŊŋį”¨åŒäš‰č¯æˆ–æ›´é€šį”¨įš„å…ŗé”Žč¯", "no_shared_albums_message": "创åģēį›¸å†ŒäģĨå…ąäēĢį…§į‰‡å’Œč§†éĸ‘", + "no_uploads_in_progress": "æ˛Ąæœ‰æ­Ŗåœ¨čŋ›čĄŒįš„上äŧ ", "not_in_any_album": "不在äģģäŊ•į›¸å†Œä¸­", "not_selected": "æœĒ选拊", "note_apply_storage_label_to_previously_uploaded assets": "提į¤ēīŧščĻå°†å­˜å‚¨æ ‡į­žåē”ᔍäēŽäš‹å‰ä¸Šäŧ įš„éĄšį›ŽīŧŒéœ€čρčŋčĄŒ", @@ -1329,6 +1368,7 @@ "original": "原回", "other": "å…ļ厃", "other_devices": "å…ļåŽƒčŽžå¤‡", + "other_entities": "å…ļäģ–厞äŊ“", "other_variables": "å…ļ厃变量", "owned": "æˆ‘įš„", "owner": "æ‰€æœ‰č€…", @@ -1460,6 +1500,7 @@ "purchase_server_description_2": "æ”¯æŒč€…įŠļ态", "purchase_server_title": "æœåŠĄå™¨", "purchase_settings_server_activated": "æœåŠĄå™¨äē§å“å¯†é’Ĩæ­Ŗåœ¨į”ąįŽĄį†å‘˜įŽĄį†", + "queue_status": "排队中 {count}/{total}", "rating": "星įē§", "rating_clear": "删除星įē§", "rating_count": "{count, plural, one {#星} other {#星}}", @@ -1488,6 +1529,8 @@ "refreshing_faces": "æ­Ŗåœ¨éĸéƒ¨é‡æ–°č¯†åˆĢ", "refreshing_metadata": "æ­Ŗåœ¨åˆˇæ–°å…ƒæ•°æŽ", "regenerating_thumbnails": "æ­Ŗåœ¨é‡æ–°į”ŸæˆįŧŠį•Ĩ回", + "remote": "čŋœį¨‹", + "remote_assets": "čŋœį¨‹éĄšį›Ž", "remove": "į§ģ除", "remove_assets_album_confirmation": "įĄŽåŽščρäģŽå›žåē“中į§ģ除{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}īŧŸ", "remove_assets_shared_link_confirmation": "įĄŽåŽščρäģŽå…ąäēĢ链æŽĨ中į§ģ除{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}īŧŸ", @@ -1495,7 +1538,9 @@ "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": "äģŽå…ąäēĢ链æŽĨ中į§ģ除", @@ -1523,11 +1568,15 @@ "reset_password": "重įŊŽå¯†į ", "reset_people_visibility": "重įŊŽäēēį‰Šč¯†åˆĢ", "reset_pin_code": "重įŊŽPIN᠁", + "reset_sqlite": "重įŊŽ SQLite 数捎åē“", + "reset_sqlite_confirmation": "æ‚¨įĄŽåŽščĻé‡įŊŽ SQLite 数捎åē“吗īŧŸæ‚¨éœ€čĻæŗ¨é”€åšļ重新į™ģåŊ•才čƒŊ重新同æ­Ĩ数捎", + "reset_sqlite_success": "åˇ˛æˆåŠŸé‡įŊŽ SQLite 数捎åē“", "reset_to_default": "æĸ复éģ˜čޤå€ŧ", "resolve_duplicates": "å¤„į†é‡å¤éĄš", "resolved_all_duplicates": "å¤„į†æ‰€æœ‰é‡å¤éĄš", "restore": "æĸ复", "restore_all": "æĸ复全部", + "restore_trash_action_prompt": "äģŽå›žæ”ļį̙䏭æĸ复äē† {count} 饚", "restore_user": "æĸå¤į”¨æˆˇ", "restored_asset": "厞æĸå¤éĄšį›Ž", "resume": "įģ§įģ­", @@ -1536,6 +1585,7 @@ "role": "é€‰æ‹Šį”¨æˆˇæƒé™", "role_editor": "可įŧ–čž‘", "role_viewer": "äģ…æŸĨįœ‹", + "running": "æ­Ŗåœ¨čŋčĄŒ", "save": "äŋå­˜", "save_to_gallery": "äŋå­˜åˆ°å›žåē“", "saved_api_key": "厞äŋå­˜įš„ API 密é’Ĩ", @@ -1667,6 +1717,7 @@ "settings_saved": "莞įŊŽåˇ˛äŋå­˜", "setup_pin_code": "莞įŊŽPIN᠁", "share": "å…ąäēĢ", + "share_action_prompt": "åˇ˛å…ąäēĢ {count} éĄšį›Ž", "share_add_photos": "æˇģåŠ éĄšį›Ž", "share_assets_selected": "{count} åˇ˛é€‰æ‹Š", "share_dialog_preparing": "æ­Ŗåœ¨å‡†å¤‡...", @@ -1768,6 +1819,7 @@ "sort_title": "标éĸ˜", "source": "GitHub æēäģŖį ", "stack": "堆叠", + "stack_action_prompt": "{count} ä¸Ēåˇ˛å †å ", "stack_duplicates": "å †å é‡å¤éĄšį›Ž", "stack_select_one_photo": "ä¸ē堆叠选拊一åŧ åą•į¤ē回", "stack_selected_photos": "å †å é€‰åŽšįš„į…§į‰‡", @@ -1787,6 +1839,7 @@ "storage_quota": "存储配éĸ", "storage_usage": "厞ᔍīŧš{used}/{available}", "submit": "提äē¤", + "success": "成功", "suggestions": "åģē莎", "sunrise_on_the_beach": "æĩˇæģŠä¸Šįš„æ—Ĩå‡ē", "support": "支持", @@ -1796,6 +1849,8 @@ "sync": "同æ­Ĩ", "sync_albums": "同æ­Ĩį›¸å†Œ", "sync_albums_manual_subtitle": "将所有上äŧ įš„视éĸ‘å’Œį…§į‰‡åŒæ­Ĩåˆ°é€‰åŽšįš„å¤‡äģŊį›¸å†Œ", + "sync_local": "同æ­ĨæœŦ地", + "sync_remote": "同æ­Ĩčŋœį¨‹", "sync_upload_album_setting_subtitle": "创åģēį…§į‰‡å’Œč§†éĸ‘åšļ上äŧ åˆ° Immich ä¸Šįš„é€‰åŽšį›¸å†Œä¸­", "tag": "æ ‡į­ž", "tag_assets": "æ ‡čŽ°éĄšį›Ž", @@ -1806,6 +1861,7 @@ "tag_updated": "åˇ˛æ›´æ–°æ ‡į­žīŧš{tag}", "tagged_assets": "{count, plural, one {# ä¸ĒéĄšį›Ž} other {# ä¸ĒéĄšį›Ž}}čĸĢåŠ ä¸Šæ ‡į­ž", "tags": "æ ‡į­ž", + "tap_to_run_job": "į‚šå‡ģčŋčĄŒäŊœä¸š", "template": "æ¨Ąį‰ˆ", "theme": "ä¸ģéĸ˜", "theme_selection": "ä¸ģéĸ˜é€‰éĄš", @@ -1838,6 +1894,7 @@ "total": "æ€ģ莥", "total_usage": "æ€ģį”¨é‡", "trash": "回æ”ļįĢ™", + "trash_action_prompt": "厞将 {count} 饚į§ģč‡ŗå›žæ”ļįĢ™", "trash_all": "全部删除", "trash_count": "删除{count, number}饚", "trash_delete_asset": "å°†éĄšį›Žæ”žå…Ĩ回æ”ļįĢ™/删除", @@ -1855,9 +1912,11 @@ "unable_to_change_pin_code": "æ— æŗ•äŋŽæ”šPIN᠁", "unable_to_setup_pin_code": "æ— æŗ•čŽžįŊŽPIN᠁", "unarchive": "取æļˆåŊ’æĄŖ", + "unarchive_action_prompt": "厞äģŽåŊ’æĄŖä¸­į§ģ除 {count} 饚", "unarchived_count": "{count, plural, other {取æļˆåŊ’æĄŖ # 饚}}", "undo": "撤销", "unfavorite": "取æļˆæ”ļ藏", + "unfavorite_action_prompt": "厞äģŽæ”ļ藏中į§ģ除 {count} 饚", "unhide_person": "昞į¤ēäēēį‰Š", "unknown": "æœĒįŸĨ", "unknown_country": "æœĒįŸĨįš„å›ŊåŽļ", @@ -1875,12 +1934,15 @@ "unselect_all_duplicates": "取æļˆé€‰æ‹Šæ‰€æœ‰é‡å¤éĄš", "unselect_all_in": "取æļˆé€‰æ‹Š {group} ä¸­įš„æ‰€æœ‰å†…åŽš", "unstack": "取æļˆå †å ", + "unstack_action_prompt": "{count} ä¸ĒæœĒ堆叠", "unstacked_assets_count": "{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}åˇ˛å–æļˆå †å ", + "untagged": "æ— æ ‡į­ž", "up_next": "下一ä¸Ē", "updated_at": "åˇ˛æ›´æ–°", "updated_password": "æ›´æ–°å¯†į ", "upload": "上äŧ ", "upload_concurrency": "上äŧ åšļ发", + "upload_details": "上äŧ č¯Ļ情", "upload_dialog_info": "是åĻčĻå°†æ‰€é€‰éĄšį›Žå¤‡äģŊåˆ°æœåŠĄå™¨īŧŸ", "upload_dialog_title": "上äŧ éĄšį›Ž", "upload_errors": "上äŧ åŽŒæˆīŧŒå‡ēįŽ°{count, plural, one {#ä¸Ē错蝝} other {#ä¸Ē错蝝}}īŧŒåˆˇæ–°éĄĩéĸäģĨæŸĨįœ‹æ–°ä¸Šäŧ įš„éĄšį›Žã€‚", @@ -1912,6 +1974,7 @@ "user_usage_stats_description": "æŸĨįœ‹å¸æˆˇäŊŋᔍįģŸčŽĄäŋĄæ¯", "username": "į”¨æˆˇå", "users": "į”¨æˆˇ", + "users_added_to_album_count": "厞将 {count, plural, one {# ä¸Ēį”¨æˆˇ} other {# ä¸Ēį”¨æˆˇ}} æˇģåŠ åˆ°į›¸å†Œ", "utilities": "åŽžį”¨åˇĨå…ˇ", "validate": "énj蝁", "validate_endpoint_error": "č¯ˇčž“å…Ĩæœ‰æ•ˆįš„ URL", @@ -1930,6 +1993,7 @@ "view_album": "æŸĨįœ‹į›¸å†Œ", "view_all": "æŸĨįœ‹å…¨éƒ¨", "view_all_users": "æŸĨįœ‹å…¨éƒ¨į”¨æˆˇ", + "view_details": "æŸĨįœ‹č¯Ļ情", "view_in_timeline": "在æ—ļ间čŊ´ä¸­æŸĨįœ‹", "view_link": "æŸĨįœ‹é“žæŽĨ", "view_links": "æŸĨįœ‹é“žæŽĨ", diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 1f4d7aa635..372982af67 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:d2621a9f74d31a8a60af19f97b09cc3ac54382c8680b6544018713a12ef6c048 AS builder-cpu +FROM python:3.11-bookworm@sha256:ce3b954c9285a7a145cba620bae03db836ab890b6b9e0d05a3ca522ea00dfbc9 AS builder-cpu FROM builder-cpu AS builder-openvino @@ -59,7 +59,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ RUN apt-get update && apt-get install -y --no-install-recommends g++ -COPY --from=ghcr.io/astral-sh/uv:latest@sha256:4faec156e35a5f345d57804d8858c6ba1cf6352ce5f4bffc11b7fdebdef46a38 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:latest@sha256:9653efd4380d5a0e5511e337dcfc3b8ba5bc4e6ea7fa3be7716598261d5503fa /uv /uvx /bin/ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ @@ -68,11 +68,11 @@ RUN if [ "$DEVICE" = "rocm" ]; then \ uv pip install /opt/onnxruntime_rocm-*.whl; \ fi -FROM python:3.11-slim-bookworm@sha256:7a3ed1226224bcc1fe5443262363d42f48cf832a540c1836ba8ccbeaadf8637c AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:9e1912aab0a30bbd9488eb79063f68f42a68ab0946cbe98fecf197fe5b085506 AS prod-cpu ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 -FROM python:3.11-slim-bookworm@sha256:7a3ed1226224bcc1fe5443262363d42f48cf832a540c1836ba8ccbeaadf8637c AS prod-openvino +FROM python:3.11-slim-bookworm@sha256:9e1912aab0a30bbd9488eb79063f68f42a68ab0946cbe98fecf197fe5b085506 AS prod-openvino RUN apt-get update && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ diff --git a/machine-learning/scripts/healthcheck.py b/machine-learning/scripts/healthcheck.py index 8a4ec897f0..82c6cad790 100644 --- a/machine-learning/scripts/healthcheck.py +++ b/machine-learning/scripts/healthcheck.py @@ -4,9 +4,12 @@ import sys import requests port = os.getenv("IMMICH_PORT", 3003) +host = os.getenv("IMMICH_HOST", "0.0.0.0") + +host = "localhost" if host == "0.0.0.0" else host try: - response = requests.get(f"http://localhost:{port}/ping", timeout=2) + response = requests.get(f"http://{host}:{port}/ping", timeout=2) if response.status_code == 200: sys.exit(0) sys.exit(1) diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 115ea259f3..7da2fd3920 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -517,16 +517,16 @@ wheels = [ [[package]] name = "fastapi" -version = "0.115.12" +version = "0.115.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" }, + { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" }, ] [[package]] @@ -900,7 +900,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.32.4" +version = "0.33.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -912,9 +912,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/c8/4f7d270285c46324fd66f62159eb16739aa5696f422dba57678a8c6b78e9/huggingface_hub-0.32.4.tar.gz", hash = "sha256:f61d45cd338736f59fb0e97550b74c24ee771bcc92c05ae0766b9116abe720be", size = 424494, upload-time = "2025-06-03T09:59:46.105Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/42/8a95c5632080ae312c0498744b2b852195e10b05a20b1be11c5141092f4c/huggingface_hub-0.33.2.tar.gz", hash = "sha256:84221defaec8fa09c090390cd68c78b88e3c4c2b7befba68d3dc5aacbc3c2c5f", size = 426637, upload-time = "2025-07-02T06:26:05.156Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/8b/222140f3cfb6f17b0dd8c4b9a0b36bd4ebefe9fb0098ba35d6960abcda0f/huggingface_hub-0.32.4-py3-none-any.whl", hash = "sha256:37abf8826b38d971f60d3625229221c36e53fe58060286db9baf619cfbf39767", size = 512101, upload-time = "2025-06-03T09:59:44.099Z" }, + { url = "https://files.pythonhosted.org/packages/44/f4/5f3f22e762ad1965f01122b42dae5bf0e009286e2dba601ce1d0dba72424/huggingface_hub-0.33.2-py3-none-any.whl", hash = "sha256:3749498bfa91e8cde2ddc2c1db92c79981f40e66434c20133b39e5928ac9bcc5", size = 515373, upload-time = "2025-07-02T06:26:03.072Z" }, ] [[package]] @@ -1044,7 +1044,7 @@ requires-dist = [ { name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.15.0,<2" }, { name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.15.0,<2" }, { name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.15.0,<2" }, - { name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" }, + { name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple" }, { name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.17.1,<1.19.0" }, { name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" }, { name = "orjson", specifier = ">=3.9.5" }, @@ -1225,7 +1225,7 @@ wheels = [ [[package]] name = "locust" -version = "2.37.9" +version = "2.37.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, @@ -1245,14 +1245,14 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/05/2bfdf19756c6a12f6f9513f75340ecf0595d83cab4d9fc91162225908e3d/locust-2.37.9.tar.gz", hash = "sha256:e43673b594ec5ecde4f9ba6e0d5c66c00d7c0ae93591951abe83e8d186c67175", size = 2252507, upload-time = "2025-06-05T09:26:58.186Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/19/66cdab585f7d4385be615d3792402fc75a1bed7519e5283adbe7133dbc78/locust-2.37.11.tar.gz", hash = "sha256:89c79bc599aa57160bd41dd3876e35d8b9dee5abded78e35008d01fd8f1640ed", size = 2252602, upload-time = "2025-06-23T08:22:23.922Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/1c/0ece4176231c578e819d970ec08d124492833e50aafd171c582bcc414446/locust-2.37.9-py3-none-any.whl", hash = "sha256:e17da439f3a252d1fb6d4c34daf00d7e8b87e99d833a32e8a79f4f8ebb07767d", size = 2269084, upload-time = "2025-06-05T09:26:56.257Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2d/e5ae05911521bf84113be349d51b16d54589e986837d2d518f63434ea3ec/locust-2.37.11-py3-none-any.whl", hash = "sha256:b826f95fbfd5d9a32df6ab1b74672b88e65bbc33ec99fdc10af98079952ad517", size = 2269179, upload-time = "2025-06-23T08:22:21.067Z" }, ] [[package]] name = "locust-cloud" -version = "1.23.1" +version = "1.24.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, @@ -1262,9 +1262,9 @@ dependencies = [ { name = "python-socketio", extra = ["client"] }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/7c/d9cbbd051490aeedfbd6ddda8ad48f77dd848ee490f6ebd166d20db5911e/locust_cloud-1.23.1.tar.gz", hash = "sha256:a09161752b8c9a9205e97cef5223ee3ad967bc2d91c52d61952aaa3da6802a55", size = 450937, upload-time = "2025-06-05T06:07:53.773Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/77/bda24167a2b763ba5d3cad1f3fa2a938f5273e51a61bffdbc8dc2e3ba24d/locust_cloud-1.24.2.tar.gz", hash = "sha256:a2656537ff367e6d4d4673477ba9e81ed73a8423a71573cd2512248740eded77", size = 451122, upload-time = "2025-06-23T11:08:00.558Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/01/5af43edee540e38ba0ee0a2e3beb72c50073e0f646bb543a8b34650315e3/locust_cloud-1.23.1-py3-none-any.whl", hash = "sha256:11677895c6ed6d0beef1b425a6f04f10ea2cfcaeaefbf00a97fb3c9134296e54", size = 408323, upload-time = "2025-06-05T06:07:51.947Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/8cda8aa1c6dfe5c5abbf69a9b10c03585c37eff64ca92733a291806052ac/locust_cloud-1.24.2-py3-none-any.whl", hash = "sha256:64a5e6f2bf0a1a012d9805291d44fb57e57535c2b5c0fa5bc87ba0d7cce9ef9c", size = 408594, upload-time = "2025-06-23T11:07:59.092Z" }, ] [[package]] @@ -1415,7 +1415,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.16.0" +version = "1.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, @@ -1423,33 +1423,33 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/5e/a0485f0608a3d67029d3d73cec209278b025e3493a3acfda3ef3a88540fd/mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", size = 10967416, upload-time = "2025-05-29T13:34:17.783Z" }, - { url = "https://files.pythonhosted.org/packages/4b/53/5837c221f74c0d53a4bfc3003296f8179c3a2a7f336d7de7bbafbe96b688/mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", size = 10087654, upload-time = "2025-05-29T13:32:37.878Z" }, - { url = "https://files.pythonhosted.org/packages/29/59/5fd2400352c3093bed4c09017fe671d26bc5bb7e6ef2d4bf85f2a2488104/mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", size = 11875192, upload-time = "2025-05-29T13:34:54.281Z" }, - { url = "https://files.pythonhosted.org/packages/ad/3e/4bfec74663a64c2012f3e278dbc29ffe82b121bc551758590d1b6449ec0c/mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", size = 12612939, upload-time = "2025-05-29T13:33:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/88/1f/fecbe3dcba4bf2ca34c26ca016383a9676711907f8db4da8354925cbb08f/mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b", size = 12874719, upload-time = "2025-05-29T13:21:52.09Z" }, - { url = "https://files.pythonhosted.org/packages/f3/51/c2d280601cd816c43dfa512a759270d5a5ef638d7ac9bea9134c8305a12f/mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", size = 9487053, upload-time = "2025-05-29T13:33:29.797Z" }, - { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498, upload-time = "2025-05-29T13:18:54.066Z" }, - { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755, upload-time = "2025-05-29T13:34:00.851Z" }, - { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138, upload-time = "2025-05-29T13:32:55.082Z" }, - { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156, upload-time = "2025-05-29T13:19:12.963Z" }, - { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426, upload-time = "2025-05-29T13:20:22.72Z" }, - { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319, upload-time = "2025-05-29T13:21:17.582Z" }, - { url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927, upload-time = "2025-05-29T13:35:52.328Z" }, - { url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082, upload-time = "2025-05-29T13:35:33.378Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306, upload-time = "2025-05-29T13:21:02.164Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764, upload-time = "2025-05-29T13:20:42.826Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233, upload-time = "2025-05-29T13:18:37.446Z" }, - { url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547, upload-time = "2025-05-29T13:20:02.836Z" }, - { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" }, - { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" }, - { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" }, - { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" }, - { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" }, - { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/8e/12/2bf23a80fcef5edb75de9a1e295d778e0f46ea89eb8b115818b663eff42b/mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a", size = 10958644, upload-time = "2025-06-16T16:51:11.649Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/bfe47b3b278eacf348291742fd5e6613bbc4b3434b72ce9361896417cfe5/mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72", size = 10087033, upload-time = "2025-06-16T16:35:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/40307c12fe25675a0776aaa2cdd2879cf30d99eec91b898de00228dc3ab5/mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea", size = 11875645, upload-time = "2025-06-16T16:35:48.49Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d8/85bdb59e4a98b7a31495bd8f1a4445d8ffc86cde4ab1f8c11d247c11aedc/mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574", size = 12616986, upload-time = "2025-06-16T16:48:39.526Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d0/bb25731158fa8f8ee9e068d3e94fcceb4971fedf1424248496292512afe9/mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d", size = 12878632, upload-time = "2025-06-16T16:36:08.195Z" }, + { url = "https://files.pythonhosted.org/packages/2d/11/822a9beb7a2b825c0cb06132ca0a5183f8327a5e23ef89717c9474ba0bc6/mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6", size = 9484391, upload-time = "2025-06-16T16:37:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" }, + { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" }, + { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" }, + { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634, upload-time = "2025-06-16T16:50:34.441Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584, upload-time = "2025-06-16T16:34:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886, upload-time = "2025-06-16T16:36:43.589Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" }, ] [[package]] @@ -1568,7 +1568,7 @@ wheels = [ [[package]] name = "onnxruntime-gpu" version = "1.19.2" -source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" } +source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple" } dependencies = [ { name = "coloredlogs" }, { name = "flatbuffers" }, @@ -1834,7 +1834,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.5" +version = "2.11.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1842,9 +1842,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, ] [[package]] @@ -1936,16 +1936,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.9.1" +version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" }, + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, ] [[package]] @@ -1977,7 +1977,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.0" +version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1988,9 +1988,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797, upload-time = "2025-06-02T17:36:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] [[package]] @@ -2007,15 +2007,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "6.1.1" +version = "6.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] [[package]] @@ -2303,27 +2304,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.11.13" +version = "0.12.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" }, - { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" }, - { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" }, - { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" }, - { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" }, - { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" }, - { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" }, - { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" }, - { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" }, + { url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" }, + { url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" }, + { url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" }, + { url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" }, + { url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" }, + { url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" }, + { url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" }, + { url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" }, + { url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" }, + { url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" }, + { url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" }, ] [[package]] @@ -2503,27 +2504,27 @@ wheels = [ [[package]] name = "tokenizers" -version = "0.21.1" +version = "0.21.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256, upload-time = "2025-03-13T10:51:18.189Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/2d/b0fce2b8201635f60e8c95990080f58461cc9ca3d5026de2e900f38a7f21/tokenizers-0.21.2.tar.gz", hash = "sha256:fdc7cffde3e2113ba0e6cc7318c40e3438a4d74bbc62bf04bcc63bdfb082ac77", size = 351545, upload-time = "2025-06-24T10:24:52.449Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767, upload-time = "2025-03-13T10:51:09.459Z" }, - { url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555, upload-time = "2025-03-13T10:51:07.692Z" }, - { url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541, upload-time = "2025-03-13T10:50:56.679Z" }, - { url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058, upload-time = "2025-03-13T10:50:59.525Z" }, - { url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278, upload-time = "2025-03-13T10:51:04.678Z" }, - { url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253, upload-time = "2025-03-13T10:51:01.261Z" }, - { url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225, upload-time = "2025-03-13T10:51:03.243Z" }, - { url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874, upload-time = "2025-03-13T10:51:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448, upload-time = "2025-03-13T10:51:10.927Z" }, - { url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877, upload-time = "2025-03-13T10:51:12.688Z" }, - { url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645, upload-time = "2025-03-13T10:51:14.723Z" }, - { url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380, upload-time = "2025-03-13T10:51:16.526Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506, upload-time = "2025-03-13T10:51:20.643Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481, upload-time = "2025-03-13T10:51:19.243Z" }, + { url = "https://files.pythonhosted.org/packages/1d/cc/2936e2d45ceb130a21d929743f1e9897514691bec123203e10837972296f/tokenizers-0.21.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:342b5dfb75009f2255ab8dec0041287260fed5ce00c323eb6bab639066fef8ec", size = 2875206, upload-time = "2025-06-24T10:24:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e6/33f41f2cc7861faeba8988e7a77601407bf1d9d28fc79c5903f8f77df587/tokenizers-0.21.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:126df3205d6f3a93fea80c7a8a266a78c1bd8dd2fe043386bafdd7736a23e45f", size = 2732655, upload-time = "2025-06-24T10:24:41.56Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1791eb329c07122a75b01035b1a3aa22ad139f3ce0ece1b059b506d9d9de/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a32cd81be21168bd0d6a0f0962d60177c447a1aa1b1e48fa6ec9fc728ee0b12", size = 3019202, upload-time = "2025-06-24T10:24:31.791Z" }, + { url = "https://files.pythonhosted.org/packages/05/15/fd2d8104faa9f86ac68748e6f7ece0b5eb7983c7efc3a2c197cb98c99030/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8bd8999538c405133c2ab999b83b17c08b7fc1b48c1ada2469964605a709ef91", size = 2934539, upload-time = "2025-06-24T10:24:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2e/53e8fd053e1f3ffbe579ca5f9546f35ac67cf0039ed357ad7ec57f5f5af0/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e9944e61239b083a41cf8fc42802f855e1dca0f499196df37a8ce219abac6eb", size = 3248665, upload-time = "2025-06-24T10:24:39.024Z" }, + { url = "https://files.pythonhosted.org/packages/00/15/79713359f4037aa8f4d1f06ffca35312ac83629da062670e8830917e2153/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:514cd43045c5d546f01142ff9c79a96ea69e4b5cda09e3027708cb2e6d5762ab", size = 3451305, upload-time = "2025-06-24T10:24:36.133Z" }, + { url = "https://files.pythonhosted.org/packages/38/5f/959f3a8756fc9396aeb704292777b84f02a5c6f25c3fc3ba7530db5feb2c/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b9405822527ec1e0f7d8d2fdb287a5730c3a6518189c968254a8441b21faae", size = 3214757, upload-time = "2025-06-24T10:24:37.784Z" }, + { url = "https://files.pythonhosted.org/packages/c5/74/f41a432a0733f61f3d21b288de6dfa78f7acff309c6f0f323b2833e9189f/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed9a4d51c395103ad24f8e7eb976811c57fbec2af9f133df471afcd922e5020", size = 3121887, upload-time = "2025-06-24T10:24:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6a/bc220a11a17e5d07b0dfb3b5c628621d4dcc084bccd27cfaead659963016/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c41862df3d873665ec78b6be36fcc30a26e3d4902e9dd8608ed61d49a48bc19", size = 9091965, upload-time = "2025-06-24T10:24:44.431Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bd/ac386d79c4ef20dc6f39c4706640c24823dca7ebb6f703bfe6b5f0292d88/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed21dc7e624e4220e21758b2e62893be7101453525e3d23264081c9ef9a6d00d", size = 9053372, upload-time = "2025-06-24T10:24:46.455Z" }, + { url = "https://files.pythonhosted.org/packages/63/7b/5440bf203b2a5358f074408f7f9c42884849cd9972879e10ee6b7a8c3b3d/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:0e73770507e65a0e0e2a1affd6b03c36e3bc4377bd10c9ccf51a82c77c0fe365", size = 9298632, upload-time = "2025-06-24T10:24:48.446Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d2/faa1acac3f96a7427866e94ed4289949b2524f0c1878512516567d80563c/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:106746e8aa9014a12109e58d540ad5465b4c183768ea96c03cbc24c44d329958", size = 9470074, upload-time = "2025-06-24T10:24:50.378Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a5/896e1ef0707212745ae9f37e84c7d50269411aef2e9ccd0de63623feecdf/tokenizers-0.21.2-cp39-abi3-win32.whl", hash = "sha256:cabda5a6d15d620b6dfe711e1af52205266d05b379ea85a8a301b3593c60e962", size = 2330115, upload-time = "2025-06-24T10:24:55.069Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/cc2755ee10be859c4338c962a35b9a663788c0c0b50c0bdd8078fb6870cf/tokenizers-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:58747bb898acdb1007f37a7bbe614346e98dc28708ffb66a3fd50ce169ac6c98", size = 2509918, upload-time = "2025-06-24T10:24:53.71Z" }, ] [[package]] @@ -2558,14 +2559,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.32.0.20250602" +version = "2.32.4.20250611" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/b0/5321e6eeba5d59e4347fcf9bf06a5052f085c3aa0f4876230566d6a4dc97/types_requests-2.32.0.20250602.tar.gz", hash = "sha256:ee603aeefec42051195ae62ca7667cd909a2f8128fdf8aad9e8a5219ecfab3bf", size = 23042, upload-time = "2025-06-02T03:15:02.958Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/18/9b782980e575c6581d5c0c1c99f4c6f89a1d7173dad072ee96b2756c02e6/types_requests-2.32.0.20250602-py3-none-any.whl", hash = "sha256:f4f335f87779b47ce10b8b8597b409130299f6971ead27fead4fe7ba6ea3e726", size = 20638, upload-time = "2025-06-02T03:15:01.959Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" }, ] [[package]] @@ -2627,16 +2628,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.34.3" +version = "0.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, ] [package.optional-dependencies] diff --git a/mobile/.fvmrc b/mobile/.fvmrc index b987073ac6..3ca65ffc7c 100644 --- a/mobile/.fvmrc +++ b/mobile/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.29.3" + "flutter": "3.32.8" } \ No newline at end of file diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json index 9c5244f098..9a9fb67ce3 100644 --- a/mobile/.vscode/settings.json +++ b/mobile/.vscode/settings.json @@ -1,5 +1,9 @@ { - "dart.flutterSdkPath": ".fvm/versions/3.29.3", + "dart.flutterSdkPath": ".fvm/versions/3.32.8", + "dart.lineLength": 120, + "[dart]": { + "editor.rulers": [120], + }, "search.exclude": { "**/.fvm": true }, diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 7ca6b7a2b8..1b0b7170d2 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -9,6 +9,9 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +formatter: + page_width: 120 + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` @@ -106,6 +109,7 @@ custom_lint: - 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 @@ -146,6 +150,7 @@ dart_code_metrics: # - no-empty-block # - no-equal-then-else # - prefer-correct-test-file-name + - prefer-const-border-radius # - prefer-match-file-name # - prefer-return-await # - avoid-self-assignment @@ -290,7 +295,8 @@ dart_code_metrics: # Style # - prefer-trailing-comma # - unnecessary-trailing-comma - # - prefer-declaring-const-constructor + - prefer-declaring-const-constructor # - prefer-single-widget-per-file + - prefer-switch-expression # - prefer-prefixed-global-constants # - prefer-correct-callback-field-name diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 0d07228252..1f0e2e7675 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -3,6 +3,8 @@ plugins { id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" id 'com.google.devtools.ksp' + id 'org.jetbrains.kotlin.plugin.compose' version '2.0.20' // this version matches your Kotlin version + } def localProperties = new Properties() @@ -45,6 +47,10 @@ android { main.java.srcDirs += 'src/main/kotlin' } + buildFeatures { + compose true + } + defaultConfig { applicationId "app.alextran.immich" minSdkVersion 26 @@ -91,6 +97,8 @@ dependencies { 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' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" @@ -102,6 +110,17 @@ dependencies { ksp "com.github.bumptech.glide:ksp:$glide_version" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' + + //Glance Widget + implementation "androidx.glance:glance-appwidget:$compose_version" + implementation "com.google.code.gson:gson:$gson_version" + + // 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" } // This is uncommented in F-Droid build script diff --git a/mobile/android/app/proguard-rules.pro b/mobile/android/app/proguard-rules.pro index ea6dd795b5..898caee06c 100644 --- a/mobile/android/app/proguard-rules.pro +++ b/mobile/android/app/proguard-rules.pro @@ -25,8 +25,15 @@ @com.google.gson.annotations.SerializedName ; } +# TypeToken preventions +-keep class com.google.gson.reflect.TypeToken { *; } +-keep class * extends com.google.gson.reflect.TypeToken + # Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. -keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken -keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken -##---------------End: proguard configuration for Gson ---------- \ No newline at end of file +##---------------End: proguard configuration for Gson ---------- + +# Keep all widget model classes and their fields for Gson +-keep class app.alextran.immich.widget.model.** { *; } \ No newline at end of file diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 2179c9eb3c..09276f6d4a 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -90,6 +90,35 @@ + + + + + + + + + + + + + + + + + + + + + @@ -112,6 +141,41 @@ android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup" tools:node="remove" /> + + + + + + + + + + + + + + + + + + + + + + + @@ -125,4 +189,4 @@ - \ No newline at end of file + 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 index 0fb75b002c..9c90528dc9 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt @@ -83,6 +83,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { runDart() + } return resolvableFuture 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 6ae4c99bd7..b5ef90310e 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 @@ -87,7 +87,8 @@ data class PlatformAsset ( val updatedAt: Long? = null, val width: Long? = null, val height: Long? = null, - val durationInSeconds: Long + val durationInSeconds: Long, + val orientation: Long ) { companion object { @@ -100,7 +101,8 @@ data class PlatformAsset ( val width = pigeonVar_list[5] as Long? val height = pigeonVar_list[6] as Long? val durationInSeconds = pigeonVar_list[7] as Long - return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds) + val orientation = pigeonVar_list[8] as Long + return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation) } } fun toList(): List { @@ -113,6 +115,7 @@ data class PlatformAsset ( width, height, durationInSeconds, + orientation, ) } override fun equals(other: Any?): Boolean { 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 9ec0d763f7..02cd54b8c3 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 @@ -5,6 +5,7 @@ import android.content.Context import android.database.Cursor import android.provider.MediaStore import android.util.Log +import androidx.core.database.getStringOrNull import java.io.File import java.io.FileInputStream import java.security.MessageDigest @@ -39,7 +40,8 @@ open class NativeSyncApiImplBase(context: Context) { MediaStore.MediaColumns.BUCKET_ID, MediaStore.MediaColumns.WIDTH, MediaStore.MediaColumns.HEIGHT, - MediaStore.MediaColumns.DURATION + MediaStore.MediaColumns.DURATION, + MediaStore.MediaColumns.ORIENTATION, ) const val HASH_BUFFER_SIZE = 2 * 1024 * 1024 @@ -73,6 +75,8 @@ open class NativeSyncApiImplBase(context: Context) { val widthColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH) val heightColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT) val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION) + val orientationColumn = + c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION) while (c.moveToNext()) { val id = c.getLong(idColumn).toString() @@ -100,6 +104,7 @@ open class NativeSyncApiImplBase(context: Context) { val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 else c.getLong(durationColumn) / 1000 val bucketId = c.getString(bucketIdColumn) + val orientation = c.getInt(orientationColumn) val asset = PlatformAsset( id, @@ -109,7 +114,8 @@ open class NativeSyncApiImplBase(context: Context) { modifiedAt, width, height, - duration + duration, + orientation.toLong(), ) yield(AssetResult.ValidAsset(asset, bucketId)) } @@ -152,7 +158,8 @@ open class NativeSyncApiImplBase(context: Context) { continue } - val name = cursor.getString(bucketNameColumn) + // MediaStore might return null for bucket name (commonly for the Root Directory), so default to "Internal Storage" + val name = cursor.getStringOrNull(bucketNameColumn) ?: "Internal Storage" val updatedAt = cursor.getLong(dateModified) albums.add(PlatformAlbum(id, name, updatedAt, false, 0)) albumsCount[id] = 1 diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt new file mode 100644 index 0000000000..9188df1700 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt @@ -0,0 +1,33 @@ +package app.alextran.immich.widget + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import java.io.File + +fun loadScaledBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap? { + val options = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeFile(file.absolutePath, options) + + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) + options.inJustDecodeBounds = false + + return BitmapFactory.decodeFile(file.absolutePath, options) +} + +fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { + val (height: Int, width: Int) = options.run { outHeight to outWidth } + var inSampleSize = 1 + + if (height > reqHeight || width > reqWidth) { + val halfHeight: Int = height / 2 + val halfWidth: Int = width / 2 + + while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { + inSampleSize *= 2 + } + } + + return inSampleSize +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt new file mode 100644 index 0000000000..3915f291f8 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt @@ -0,0 +1,244 @@ +package app.alextran.immich.widget + +import android.content.Context +import android.graphics.Bitmap +import android.util.Log +import androidx.datastore.preferences.core.Preferences +import androidx.glance.* +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.state.updateAppWidgetState +import androidx.work.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream +import java.util.UUID +import java.util.concurrent.TimeUnit +import androidx.glance.appwidget.state.getAppWidgetState +import androidx.glance.state.PreferencesGlanceStateDefinition +import app.alextran.immich.widget.model.* +import java.time.LocalDate + +class ImageDownloadWorker( + private val context: Context, + workerParameters: WorkerParameters +) : CoroutineWorker(context, workerParameters) { + + companion object { + + private val uniqueWorkName = ImageDownloadWorker::class.java.simpleName + + private fun buildConstraints(): Constraints { + return Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + } + + private fun buildInputData(appWidgetId: Int, widgetType: WidgetType): Data { + return Data.Builder() + .putString(kWorkerWidgetType, widgetType.toString()) + .putInt(kWorkerWidgetID, appWidgetId) + .build() + } + + fun enqueuePeriodic(context: Context, appWidgetId: Int, widgetType: WidgetType) { + val manager = WorkManager.getInstance(context) + + val workRequest = PeriodicWorkRequestBuilder( + 20, TimeUnit.MINUTES + ) + .setConstraints(buildConstraints()) + .setInputData(buildInputData(appWidgetId, widgetType)) + .addTag(appWidgetId.toString()) + .build() + + manager.enqueueUniquePeriodicWork( + "$uniqueWorkName-$appWidgetId", + ExistingPeriodicWorkPolicy.UPDATE, + workRequest + ) + } + + fun singleShot(context: Context, appWidgetId: Int, widgetType: WidgetType) { + val manager = WorkManager.getInstance(context) + + val workRequest = OneTimeWorkRequestBuilder() + .setConstraints(buildConstraints()) + .setInputData(buildInputData(appWidgetId, widgetType)) + .addTag(appWidgetId.toString()) + .build() + + manager.enqueueUniqueWork( + "$uniqueWorkName-$appWidgetId", + ExistingWorkPolicy.REPLACE, + workRequest + ) + } + + suspend fun cancel(context: Context, appWidgetId: Int) { + WorkManager.getInstance(context).cancelAllWorkByTag("$uniqueWorkName-$appWidgetId") + + // delete cached image + val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId) + val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId) + val currentImgUUID = widgetConfig[kImageUUID] + + if (!currentImgUUID.isNullOrEmpty()) { + val file = File(context.cacheDir, imageFilename(currentImgUUID)) + file.delete() + } + } + } + + override suspend fun doWork(): Result { + return try { + val widgetType = WidgetType.valueOf(inputData.getString(kWorkerWidgetType) ?: "") + val widgetId = inputData.getInt(kWorkerWidgetID, -1) + val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId) + val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId) + val currentImgUUID = widgetConfig[kImageUUID] + + val serverConfig = ImmichAPI.getServerConfig(context) + + // clear any image caches and go to "login" state if no credentials + if (serverConfig == null) { + if (!currentImgUUID.isNullOrEmpty()) { + deleteImage(currentImgUUID) + updateWidget( + glanceId, + "", + "", + "immich://", + WidgetState.LOG_IN + ) + } + + return Result.success() + } + + // fetch new image + val entry = when (widgetType) { + WidgetType.RANDOM -> fetchRandom(serverConfig, widgetConfig) + WidgetType.MEMORIES -> fetchMemory(serverConfig) + } + + // clear current image if it exists + if (!currentImgUUID.isNullOrEmpty()) { + deleteImage(currentImgUUID) + } + + // save a new image + val imgUUID = UUID.randomUUID().toString() + saveImage(entry.image, imgUUID) + + // trigger the update routine with new image uuid + updateWidget(glanceId, imgUUID, entry.subtitle, entry.deeplink) + + Result.success() + } catch (e: Exception) { + Log.e(uniqueWorkName, "Error while loading image", e) + if (runAttemptCount < 10) { + Result.retry() + } else { + Result.failure() + } + } + } + + private suspend fun updateWidget( + glanceId: GlanceId, + imageUUID: String, + subtitle: String?, + deeplink: String?, + widgetState: WidgetState = WidgetState.SUCCESS + ) { + updateAppWidgetState(context, glanceId) { prefs -> + prefs[kNow] = System.currentTimeMillis() + prefs[kImageUUID] = imageUUID + prefs[kWidgetState] = widgetState.toString() + prefs[kSubtitleText] = subtitle ?: "" + prefs[kDeeplinkURL] = deeplink ?: "" + } + + PhotoWidget().update(context,glanceId) + } + + private suspend fun fetchRandom( + serverConfig: ServerConfig, + widgetConfig: Preferences + ): WidgetEntry { + val api = ImmichAPI(serverConfig) + + val filters = SearchFilters() + val albumId = widgetConfig[kSelectedAlbum] + val showSubtitle = widgetConfig[kShowAlbumName] + val albumName = widgetConfig[kSelectedAlbumName] + var subtitle: String? = if (showSubtitle == true) albumName else "" + + + if (albumId == "FAVORITES") { + filters.isFavorite = true + } else if (albumId != null) { + filters.albumIds = listOf(albumId) + } + + var randomSearch = api.fetchSearchResults(filters) + + // handle an empty album, fallback to random + if (randomSearch.isEmpty() && albumId != null) { + randomSearch = api.fetchSearchResults(SearchFilters()) + subtitle = "" + } + + val random = randomSearch.first() + val image = api.fetchImage(random) + + return WidgetEntry( + image, + subtitle, + assetDeeplink(random) + ) + } + + private suspend fun fetchMemory( + serverConfig: ServerConfig + ): WidgetEntry { + val api = ImmichAPI(serverConfig) + + val today = LocalDate.now() + val memories = api.fetchMemory(today) + val asset: Asset + var subtitle: String? = null + + if (memories.isNotEmpty()) { + // pick a random asset from a random memory + val memory = memories.random() + asset = memory.assets.random() + + val yearDiff = today.year - memory.data.year + subtitle = "$yearDiff ${if (yearDiff == 1) "year" else "years"} ago" + } else { + val filters = SearchFilters(size=1) + asset = api.fetchSearchResults(filters).first() + } + + val image = api.fetchImage(asset) + return WidgetEntry( + image, + subtitle, + assetDeeplink(asset) + ) + } + + private suspend fun deleteImage(uuid: String) = withContext(Dispatchers.IO) { + val file = File(context.cacheDir, imageFilename(uuid)) + file.delete() + } + + private suspend fun saveImage(bitmap: Bitmap, uuid: String) = withContext(Dispatchers.IO) { + val file = File(context.cacheDir, imageFilename(uuid)) + FileOutputStream(file).use { out -> + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out) + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt new file mode 100644 index 0000000000..42f5fb4b1b --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt @@ -0,0 +1,103 @@ +package app.alextran.immich.widget + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import app.alextran.immich.widget.model.* +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import es.antonborri.home_widget.HomeWidgetPlugin +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.OutputStreamWriter +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLEncoder +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +class ImmichAPI(cfg: ServerConfig) { + + companion object { + fun getServerConfig(context: Context): ServerConfig? { + val prefs = HomeWidgetPlugin.getData(context) + + val serverURL = prefs.getString("widget_server_url", "") ?: "" + val sessionKey = prefs.getString("widget_auth_token", "") ?: "" + + if (serverURL.isBlank() || sessionKey.isBlank()) { + return null + } + + return ServerConfig( + serverURL, + sessionKey + ) + } + } + + + private val gson = Gson() + private val serverConfig = cfg + + private fun buildRequestURL(endpoint: String, params: List> = emptyList()): URL { + val urlString = StringBuilder("${serverConfig.serverEndpoint}$endpoint?sessionKey=${serverConfig.sessionKey}") + + for ((key, value) in params) { + urlString.append("&${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}") + } + + return URL(urlString.toString()) + } + + suspend fun fetchSearchResults(filters: SearchFilters): List = withContext(Dispatchers.IO) { + val url = buildRequestURL("/search/random") + val connection = (url.openConnection() as HttpURLConnection).apply { + requestMethod = "POST" + setRequestProperty("Content-Type", "application/json") + doOutput = true + } + + connection.outputStream.use { + OutputStreamWriter(it).use { writer -> + writer.write(gson.toJson(filters)) + writer.flush() + } + } + + val response = connection.inputStream.bufferedReader().readText() + val type = object : TypeToken>() {}.type + gson.fromJson(response, type) + } + + suspend fun fetchMemory(date: LocalDate): List = withContext(Dispatchers.IO) { + val iso8601 = date.format(DateTimeFormatter.ISO_LOCAL_DATE) + val url = buildRequestURL("/memories", listOf("for" to iso8601)) + val connection = (url.openConnection() as HttpURLConnection).apply { + requestMethod = "GET" + } + + val response = connection.inputStream.bufferedReader().readText() + val type = object : TypeToken>() {}.type + gson.fromJson(response, type) + } + + suspend fun fetchImage(asset: Asset): Bitmap = withContext(Dispatchers.IO) { + val url = buildRequestURL("/assets/${asset.id}/thumbnail", listOf("size" to "preview")) + val connection = url.openConnection() + val data = connection.getInputStream().readBytes() + BitmapFactory.decodeByteArray(data, 0, data.size) + ?: throw Exception("Invalid image data") + } + + suspend fun fetchAlbums(): List = withContext(Dispatchers.IO) { + val url = buildRequestURL("/albums") + val connection = (url.openConnection() as HttpURLConnection).apply { + requestMethod = "GET" + } + + val response = connection.inputStream.bufferedReader().readText() + val type = object : TypeToken>() {}.type + gson.fromJson(response, type) + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/MemoryReceiver.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/MemoryReceiver.kt new file mode 100644 index 0000000000..7721af7d6f --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/MemoryReceiver.kt @@ -0,0 +1,56 @@ +package app.alextran.immich.widget + +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import app.alextran.immich.widget.model.* +import es.antonborri.home_widget.HomeWidgetPlugin +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class MemoryReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget = PhotoWidget() + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + super.onUpdate(context, appWidgetManager, appWidgetIds) + + appWidgetIds.forEach { widgetID -> + ImageDownloadWorker.enqueuePeriodic(context, widgetID, WidgetType.MEMORIES) + } + } + + override fun onReceive(context: Context, intent: Intent) { + val fromMainApp = intent.getBooleanExtra(HomeWidgetPlugin.TRIGGERED_FROM_HOME_WIDGET, false) + + // Launch coroutine to setup a single shot if the app requested the update + if (fromMainApp) { + CoroutineScope(Dispatchers.Default).launch { + val provider = ComponentName(context, MemoryReceiver::class.java) + val glanceIds = AppWidgetManager.getInstance(context).getAppWidgetIds(provider) + + glanceIds.forEach { widgetID -> + ImageDownloadWorker.singleShot(context, widgetID, WidgetType.MEMORIES) + } + } + } + + super.onReceive(context, intent) + } + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + super.onDeleted(context, appWidgetIds) + CoroutineScope(Dispatchers.Default).launch { + appWidgetIds.forEach { id -> + ImageDownloadWorker.cancel(context, id) + } + } + } +} + diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt new file mode 100644 index 0000000000..b1a0a9de31 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt @@ -0,0 +1,124 @@ +package app.alextran.immich.widget + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.* +import androidx.core.net.toUri +import androidx.datastore.preferences.core.MutablePreferences +import androidx.glance.appwidget.* +import androidx.glance.* +import androidx.glance.action.clickable +import androidx.glance.layout.* +import androidx.glance.state.GlanceStateDefinition +import androidx.glance.state.PreferencesGlanceStateDefinition +import androidx.glance.text.Text +import androidx.glance.text.TextAlign +import androidx.glance.text.TextStyle +import androidx.glance.unit.ColorProvider +import app.alextran.immich.R +import app.alextran.immich.widget.model.* +import java.io.File + +class PhotoWidget : GlanceAppWidget() { + override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition + + override suspend fun provideGlance(context: Context, id: GlanceId) { + provideContent { + val prefs = currentState() + + val imageUUID = prefs[kImageUUID] + val subtitle = prefs[kSubtitleText] + val deeplinkURL = prefs[kDeeplinkURL]?.toUri() + val widgetState = prefs[kWidgetState] + var bitmap: Bitmap? = null + + if (imageUUID != null) { + // fetch a random photo from server + val file = File(context.cacheDir, imageFilename(imageUUID)) + + if (file.exists()) { + bitmap = loadScaledBitmap(file, 500, 500) + } + } + + // WIDGET CONTENT + Box( + modifier = GlanceModifier + .fillMaxSize() + .background(GlanceTheme.colors.background) + .clickable { + val intent = Intent(Intent.ACTION_VIEW, deeplinkURL ?: "immich://".toUri()) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + } + ) { + if (bitmap != null) { + Image( + provider = ImageProvider(bitmap), + contentDescription = "Widget Image", + contentScale = ContentScale.Crop, + modifier = GlanceModifier.fillMaxSize() + ) + + if (!subtitle.isNullOrBlank()) { + Column( + verticalAlignment = Alignment.Bottom, + horizontalAlignment = Alignment.Start, + modifier = GlanceModifier + .fillMaxSize() + .padding(12.dp) + ) { + Text( + text = subtitle, + style = TextStyle( + color = ColorProvider(Color.White), + fontSize = 16.sp + ), + modifier = GlanceModifier + .background(ColorProvider(Color(0x99000000))) // 60% black + .padding(8.dp) + .cornerRadius(8.dp) + ) + } + } + } else { + Column( + modifier = GlanceModifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + provider = ImageProvider(R.drawable.splash), + contentDescription = null, + ) + + if (widgetState == WidgetState.LOG_IN.toString()) { + Box( + modifier = GlanceModifier.fillMaxWidth().padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text("Log in to your Immich server", style = TextStyle(textAlign = TextAlign.Center, color = GlanceTheme.colors.primary)) + } + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = GlanceModifier.fillMaxWidth().padding(16.dp) + ) { + CircularProgressIndicator( + modifier = GlanceModifier.size(12.dp) + ) + + Spacer(modifier = GlanceModifier.width(8.dp)) + + Text("Loading widget...", style = TextStyle(textAlign = TextAlign.Center, color = GlanceTheme.colors.primary)) + } + } + } + } + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt new file mode 100644 index 0000000000..39afd76c35 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt @@ -0,0 +1,55 @@ +package app.alextran.immich.widget + +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import es.antonborri.home_widget.HomeWidgetPlugin +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import app.alextran.immich.widget.model.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class RandomReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget = PhotoWidget() + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + super.onUpdate(context, appWidgetManager, appWidgetIds) + + appWidgetIds.forEach { widgetID -> + ImageDownloadWorker.enqueuePeriodic(context, widgetID, WidgetType.RANDOM) + } + } + + override fun onReceive(context: Context, intent: Intent) { + val fromMainApp = intent.getBooleanExtra(HomeWidgetPlugin.TRIGGERED_FROM_HOME_WIDGET, false) + + // Launch coroutine to setup a single shot if the app requested the update + if (fromMainApp) { + CoroutineScope(Dispatchers.Default).launch { + val provider = ComponentName(context, RandomReceiver::class.java) + val glanceIds = AppWidgetManager.getInstance(context).getAppWidgetIds(provider) + + glanceIds.forEach { widgetID -> + ImageDownloadWorker.singleShot(context, widgetID, WidgetType.RANDOM) + } + } + } + + super.onReceive(context, intent) + } + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + super.onDeleted(context, appWidgetIds) + CoroutineScope(Dispatchers.Default).launch { + appWidgetIds.forEach { id -> + ImageDownloadWorker.cancel(context, id) + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/Dropdown.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/Dropdown.kt new file mode 100644 index 0000000000..74686ee0b8 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/Dropdown.kt @@ -0,0 +1,64 @@ +package app.alextran.immich.widget.configure + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.* + + +data class DropdownItem ( + val label: String, + val id: String, +) + +// Creating a composable to display a drop down menu +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Dropdown(items: List, + selectedItem: DropdownItem?, + onItemSelected: (DropdownItem) -> Unit, + enabled: Boolean = true +) { + + var expanded by remember { mutableStateOf(false) } + var selectedOption by remember { mutableStateOf(selectedItem?.label ?: items[0].label) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded && enabled }, + ) { + + TextField( + value = selectedOption, + onValueChange = {}, + readOnly = true, + enabled = enabled, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) + }, + colors = ExposedDropdownMenuDefaults.textFieldColors(), + modifier = Modifier + .fillMaxWidth() + .menuAnchor() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + items.forEach { option -> + DropdownMenuItem( + text = { Text(option.label, color = MaterialTheme.colorScheme.onSurface) }, + onClick = { + selectedOption = option.label + onItemSelected(option) + + expanded = false + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding + ) + } + } + } + } + diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/LightDarkTheme.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/LightDarkTheme.kt new file mode 100644 index 0000000000..efdcc41540 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/LightDarkTheme.kt @@ -0,0 +1,28 @@ +package app.alextran.immich.widget.configure + +import android.os.Build +import androidx.compose.foundation.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +@Composable +fun LightDarkTheme( + content: @Composable () -> Unit +) { + val context = LocalContext.current + val isDarkTheme = isSystemInDarkTheme() + + val colorScheme = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isDarkTheme -> + dynamicDarkColorScheme(context) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !isDarkTheme -> + dynamicLightColorScheme(context) + isDarkTheme -> darkColorScheme() + else -> lightColorScheme() + } + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/RandomConfigure.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/RandomConfigure.kt new file mode 100644 index 0000000000..83e404a8f1 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/RandomConfigure.kt @@ -0,0 +1,210 @@ +package app.alextran.immich.widget.configure + +import android.appwidget.AppWidgetManager +import android.content.Context +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import androidx.glance.GlanceId +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.state.getAppWidgetState +import androidx.glance.appwidget.state.updateAppWidgetState +import androidx.glance.state.PreferencesGlanceStateDefinition +import app.alextran.immich.widget.ImageDownloadWorker +import app.alextran.immich.widget.ImmichAPI +import app.alextran.immich.widget.model.* +import kotlinx.coroutines.launch +import java.io.FileNotFoundException + +class RandomConfigure : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Get widget ID from intent + val appWidgetId = intent?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID) + ?: AppWidgetManager.INVALID_APPWIDGET_ID + + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish() + return + } + + val glanceId = GlanceAppWidgetManager(applicationContext) + .getGlanceIdBy(appWidgetId) + + setContent { + LightDarkTheme { + RandomConfiguration(applicationContext, appWidgetId, glanceId, onDone = { + finish() + Log.w("WIDGET_ACTIVITY", "SAVING") + }) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RandomConfiguration(context: Context, appWidgetId: Int, glanceId: GlanceId, onDone: () -> Unit) { + + var selectedAlbum by remember { mutableStateOf(null) } + var showAlbumName by remember { mutableStateOf(false) } + var availableAlbums by remember { mutableStateOf>(listOf()) } + var state by remember { mutableStateOf(WidgetConfigState.LOADING) } + + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + // get albums from server + val serverCfg = ImmichAPI.getServerConfig(context) + + if (serverCfg == null) { + state = WidgetConfigState.LOG_IN + return@LaunchedEffect + } + + val api = ImmichAPI(serverCfg) + + val currentState = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId) + val currentAlbumId = currentState[kSelectedAlbum] ?: "NONE" + val currentAlbumName = currentState[kSelectedAlbumName] ?: "None" + var albumItems: List + + try { + albumItems = api.fetchAlbums().map { + DropdownItem(it.albumName, it.id) + } + + state = WidgetConfigState.SUCCESS + } catch (e: FileNotFoundException) { + Log.e("WidgetWorker", "Error fetching albums: ${e.message}") + + state = WidgetConfigState.NO_CONNECTION + albumItems = listOf(DropdownItem(currentAlbumName, currentAlbumId)) + } + + availableAlbums = listOf(DropdownItem("None", "NONE"), DropdownItem("Favorites", "FAVORITES")) + albumItems + + // load selected configuration + val albumEntity = availableAlbums.firstOrNull { it.id == currentAlbumId } + selectedAlbum = albumEntity ?: availableAlbums.first() + + // load showAlbumName + showAlbumName = currentState[kShowAlbumName] == true + } + + suspend fun saveConfiguration() { + updateAppWidgetState(context, glanceId) { prefs -> + prefs[kSelectedAlbum] = selectedAlbum?.id ?: "" + prefs[kSelectedAlbumName] = selectedAlbum?.label ?: "" + prefs[kShowAlbumName] = showAlbumName + } + + ImageDownloadWorker.singleShot(context, appWidgetId, WidgetType.RANDOM) + } + + Scaffold( + topBar = { + TopAppBar ( + title = { Text("Widget Configuration") }, + actions = { + IconButton(onClick = { + scope.launch { + saveConfiguration() + onDone() + } + }) { + Icon(Icons.Default.Check, contentDescription = "Close", tint = MaterialTheme.colorScheme.primary) + } + } + ) + } + ) { innerPadding -> + Surface( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), // Respect the top bar + color = MaterialTheme.colorScheme.background + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) { + when (state) { + WidgetConfigState.LOADING -> CircularProgressIndicator(modifier = Modifier.size(48.dp)) + WidgetConfigState.LOG_IN -> Text("You must log in inside the Immich App to configure this widget.") + else -> { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("View a random image from your library or a specific album.", style = MaterialTheme.typography.bodyMedium) + + // no connection warning + if (state == WidgetConfigState.NO_CONNECTION) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.errorContainer) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = "Warning", + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = "No connection to the server is available. Please try again later.", + style = MaterialTheme.typography.bodyMedium + ) + } + } + + Column( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("Album") + Dropdown( + items = availableAlbums, + selectedItem = selectedAlbum, + onItemSelected = { selectedAlbum = it }, + enabled = (state != WidgetConfigState.NO_CONNECTION) + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Show Album Name") + Switch( + checked = showAlbumName, + onCheckedChange = { showAlbumName = it }, + enabled = (state != WidgetConfigState.NO_CONNECTION) + ) + } + } + } + } + } + } + } + } +} + diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt new file mode 100644 index 0000000000..9595a3b696 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt @@ -0,0 +1,80 @@ +package app.alextran.immich.widget.model + +import android.graphics.Bitmap +import androidx.datastore.preferences.core.* + +// MARK: Immich Entities + +enum class AssetType { + IMAGE, VIDEO, AUDIO, OTHER +} + +data class Asset( + val id: String, + val type: AssetType, +) + +data class SearchFilters( + var type: AssetType = AssetType.IMAGE, + val size: Int = 1, + var albumIds: List = listOf(), + var isFavorite: Boolean? = null +) + +data class MemoryResult( + val id: String, + var assets: List, + val type: String, + val data: MemoryData +) { + data class MemoryData(val year: Int) +} + +data class Album( + val id: String, + val albumName: String +) + +// MARK: Widget Specific + +enum class WidgetType { + RANDOM, MEMORIES; +} + +enum class WidgetState { + LOADING, SUCCESS, LOG_IN; +} + +enum class WidgetConfigState { + LOADING, SUCCESS, LOG_IN, NO_CONNECTION +} + +data class WidgetEntry ( + val image: Bitmap, + val subtitle: String?, + val deeplink: String? +) + +data class ServerConfig(val serverEndpoint: String, val sessionKey: String) + +// MARK: Widget State Keys +val kImageUUID = stringPreferencesKey("uuid") +val kSubtitleText = stringPreferencesKey("subtitle") +val kNow = longPreferencesKey("now") +val kWidgetState = stringPreferencesKey("state") +val kSelectedAlbum = stringPreferencesKey("albumID") +val kSelectedAlbumName = stringPreferencesKey("albumName") +val kShowAlbumName = booleanPreferencesKey("showAlbumName") +val kDeeplinkURL = stringPreferencesKey("deeplink") + +const val kWorkerWidgetType = "widgetType" +const val kWorkerWidgetID = "widgetId" +const val kTriggeredFromApp = "triggeredFromApp" + +fun imageFilename(id: String): String { + return "widget_image_$id.jpg" +} + +fun assetDeeplink(asset: Asset): String { + return "immich://asset?id=${asset.id}" +} diff --git a/mobile/android/app/src/main/res/drawable-nodpi/memory_preview.png b/mobile/android/app/src/main/res/drawable-nodpi/memory_preview.png new file mode 100644 index 0000000000..97aceb3ef6 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-nodpi/memory_preview.png differ diff --git a/mobile/android/app/src/main/res/drawable-nodpi/random_preview.png b/mobile/android/app/src/main/res/drawable-nodpi/random_preview.png new file mode 100644 index 0000000000..f94d1bbcd5 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-nodpi/random_preview.png differ diff --git a/mobile/android/app/src/main/res/values/strings.xml b/mobile/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..5ac495ebb5 --- /dev/null +++ b/mobile/android/app/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + + Memories + Random + + See memories from Immich. + View a random image from your library or a specific album. + diff --git a/mobile/android/app/src/main/res/xml/memory_widget.xml b/mobile/android/app/src/main/res/xml/memory_widget.xml new file mode 100644 index 0000000000..611c5aae02 --- /dev/null +++ b/mobile/android/app/src/main/res/xml/memory_widget.xml @@ -0,0 +1,9 @@ + diff --git a/mobile/android/app/src/main/res/xml/random_widget.xml b/mobile/android/app/src/main/res/xml/random_widget.xml new file mode 100644 index 0000000000..25fb24754f --- /dev/null +++ b/mobile/android/app/src/main/res/xml/random_widget.xml @@ -0,0 +1,13 @@ + diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 31203fe55f..2287ab0821 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" => 204, - "android.injected.version.name" => "1.135.3", + "android.injected.version.code" => 205, + "android.injected.version.name" => "1.136.0", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/android/gradle.properties b/mobile/android/gradle.properties index 78c37cc2a3..f63dcd33e1 100644 --- a/mobile/android/gradle.properties +++ b/mobile/android/gradle.properties @@ -2,4 +2,6 @@ org.gradle.jvmargs=-Xmx4096M android.useAndroidX=true android.enableJetifier=true android.nonTransitiveRClass=false -android.nonFinalResIds=false \ No newline at end of file +android.nonFinalResIds=false +org.gradle.caching=true +org.gradle.parallel=true diff --git a/mobile/dcm_global.yaml b/mobile/dcm_global.yaml index d2465e64b6..c33846e674 100644 --- a/mobile/dcm_global.yaml +++ b/mobile/dcm_global.yaml @@ -1 +1 @@ -version: '>=1.29.0 <1.30.0' +version: '>=1.29.0 <=1.30.0' diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index 1b2c86026c..978a9ba8ad 100644 --- a/mobile/drift_schemas/main/drift_schema_v1.json +++ b/mobile/drift_schemas/main/drift_schema_v1.json @@ -1 +1 @@ -{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[2],"type":"index","data":{"on":2,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":4,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":5,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":6,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":7,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":8,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":9,"references":[2,8],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":10,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":11,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetOrder.values)","dart_type_name":"AssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":12,"references":[1,11],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":13,"references":[11,0],"type":"table","data":{"name":"album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}}]} \ No newline at end of file +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[0,1],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id)"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[2],"type":"index","data":{"on":2,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":5,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":6,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":7,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":8,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":9,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":10,"references":[2,9],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":11,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"lens","getter_name":"lens","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":12,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":13,"references":[1,12],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":14,"references":[12,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":15,"references":[0],"type":"table","data":{"name":"memory_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":16,"references":[1,15],"type":"table","data":{"name":"memory_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":17,"references":[0],"type":"table","data":{"name":"person_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumbnail_path","getter_name":"thumbnailPath","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}}]} \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v2.json b/mobile/drift_schemas/main/drift_schema_v2.json new file mode 100644 index 0000000000..978a9ba8ad --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v2.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[0,1],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id)"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[2],"type":"index","data":{"on":2,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":5,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":6,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":7,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":8,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":9,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":10,"references":[2,9],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":11,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"lens","getter_name":"lens","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":12,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":13,"references":[1,12],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":14,"references":[12,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":15,"references":[0],"type":"table","data":{"name":"memory_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":16,"references":[1,15],"type":"table","data":{"name":"memory_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":17,"references":[0],"type":"table","data":{"name":"person_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumbnail_path","getter_name":"thumbnailPath","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}}]} \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v3.json b/mobile/drift_schemas/main/drift_schema_v3.json new file mode 100644 index 0000000000..1acfbaf493 --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v3.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[0],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[2],"type":"index","data":{"on":2,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":5,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":6,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":7,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":8,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":9,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":10,"references":[2,9],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":11,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"lens","getter_name":"lens","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":12,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":13,"references":[1,12],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":14,"references":[12,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":15,"references":[0],"type":"table","data":{"name":"memory_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":16,"references":[1,15],"type":"table","data":{"name":"memory_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":17,"references":[0],"type":"table","data":{"name":"person_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumbnail_path","getter_name":"thumbnailPath","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}}]} \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v4.json b/mobile/drift_schemas/main/drift_schema_v4.json new file mode 100644 index 0000000000..2488319a5e --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v4.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[3,4],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":6,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":7,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":8,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":9,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":10,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":11,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"lens","getter_name":"lens","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":12,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":13,"references":[1,12],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":14,"references":[12,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":15,"references":[0],"type":"table","data":{"name":"memory_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":16,"references":[1,15],"type":"table","data":{"name":"memory_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":17,"references":[0],"type":"table","data":{"name":"person_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":18,"references":[1,17],"type":"table","data":{"name":"asset_face_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}}]} \ No newline at end of file diff --git a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart index e2622fadd1..7d3ed4757e 100644 --- a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart +++ b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart @@ -23,7 +23,7 @@ class ImmichLinter extends PluginBase { return rules; } - static makeCode(String name, LintOptions options) => LintCode( + static LintCode makeCode(String name, LintOptions options) => LintCode( name: name, problemMessage: options.json["message"] as String, errorSeverity: ErrorSeverity.WARNING, diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock index e05cbb3db3..0e4b08be87 100644 --- a/mobile/immich_lint/pubspec.lock +++ b/mobile/immich_lint/pubspec.lock @@ -5,34 +5,34 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f url: "https://pub.dev" source: hosted - version: "80.0.0" + version: "82.0.0" analyzer: dependency: "direct main" description: name: analyzer - sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.4.5" analyzer_plugin: dependency: "direct main" description: name: analyzer_plugin - sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 + sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.13.1" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" ci: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: custom_lint_visitor - sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" + sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d url: "https://pub.dev" source: hosted - version: "1.0.0+7.3.0" + version: "1.0.0+7.4.5" dart_style: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" glob: dependency: "direct main" description: @@ -213,18 +213,18 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" package_config: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: transitive description: @@ -317,10 +317,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" typed_data: dependency: transitive description: @@ -341,18 +341,18 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.0.2" watcher: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" yaml: dependency: transitive description: @@ -362,4 +362,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.8.0 <4.0.0" diff --git a/mobile/integration_test/test_utils/login_helper.dart b/mobile/integration_test/test_utils/login_helper.dart index b3a867af68..cfbc5a9214 100644 --- a/mobile/integration_test/test_utils/login_helper.dart +++ b/mobile/integration_test/test_utils/login_helper.dart @@ -7,7 +7,7 @@ import 'general_helper.dart'; class ImmichTestLoginHelper { final WidgetTester tester; - ImmichTestLoginHelper(this.tester); + const ImmichTestLoginHelper(this.tester); Future waitForLoginScreen() async { await pumpUntilFound(tester, find.text("Login")); @@ -60,11 +60,11 @@ class ImmichTestLoginHelper { await tester.tap(button); } - Future assertLoginSuccess({int timeoutSeconds = 15}) async { + Future assertLoginSuccess() async { await pumpUntilFound(tester, find.text("home_page_building_timeline".tr())); } - Future assertLoginFailed({int timeoutSeconds = 15}) async { + Future assertLoginFailed() async { await pumpUntilFound(tester, find.text("login_form_failed_login".tr())); } } diff --git a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 656d278d6d..2367c52b97 100644 --- a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> CFBundleTypeRole Editor + CFBundleURLName + Share Extension CFBundleURLSchemes ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) + + CFBundleTypeRole + Editor + CFBundleURLName + Deep Link + CFBundleURLSchemes + + immich + + CFBundleVersion 210 @@ -120,6 +132,8 @@ NSCameraUsageDescription We need to access the camera to let you take beautiful video using this app + NSFaceIDUsageDescription + We need to use FaceID to allow access to your locked folder NSLocationAlwaysAndWhenInUseUsageDescription We require this permission to access the local WiFi name for background upload mechanism NSLocationUsageDescription @@ -166,8 +180,6 @@ io.flutter.embedded_views_preview - NSFaceIDUsageDescription - We need to use FaceID to allow access to your locked folder NSLocalNetworkUsageDescription We need local network permission to connect to the local server using IP address and allow the casting feature to work diff --git a/mobile/ios/Runner/Runner.entitlements b/mobile/ios/Runner/Runner.entitlements index d558e35e0a..e5862cb213 100644 --- a/mobile/ios/Runner/Runner.entitlements +++ b/mobile/ios/Runner/Runner.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.associated-domains + + applinks:my.immich.app + com.apple.developer.networking.wifi-info com.apple.security.application-groups diff --git a/mobile/ios/Runner/RunnerProfile.entitlements b/mobile/ios/Runner/RunnerProfile.entitlements index d44633db40..6a5c086baf 100644 --- a/mobile/ios/Runner/RunnerProfile.entitlements +++ b/mobile/ios/Runner/RunnerProfile.entitlements @@ -4,6 +4,10 @@ aps-environment development + com.apple.developer.associated-domains + + applinks:my.immich.app + com.apple.developer.networking.wifi-info com.apple.security.application-groups diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift index 89eb092a16..e629604d6a 100644 --- a/mobile/ios/Runner/Sync/Messages.g.swift +++ b/mobile/ios/Runner/Sync/Messages.g.swift @@ -138,6 +138,7 @@ struct PlatformAsset: Hashable { var width: Int64? = nil var height: Int64? = nil var durationInSeconds: Int64 + var orientation: Int64 // swift-format-ignore: AlwaysUseLowerCamelCase @@ -150,6 +151,7 @@ struct PlatformAsset: Hashable { let width: Int64? = nilOrValue(pigeonVar_list[5]) let height: Int64? = nilOrValue(pigeonVar_list[6]) let durationInSeconds = pigeonVar_list[7] as! Int64 + let orientation = pigeonVar_list[8] as! Int64 return PlatformAsset( id: id, @@ -159,7 +161,8 @@ struct PlatformAsset: Hashable { updatedAt: updatedAt, width: width, height: height, - durationInSeconds: durationInSeconds + durationInSeconds: durationInSeconds, + orientation: orientation ) } func toList() -> [Any?] { @@ -172,6 +175,7 @@ struct PlatformAsset: Hashable { width, height, durationInSeconds, + orientation, ] } static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { diff --git a/mobile/ios/Runner/Sync/MessagesImpl.swift b/mobile/ios/Runner/Sync/MessagesImpl.swift index 85f3b1fcfb..459e29fa5a 100644 --- a/mobile/ios/Runner/Sync/MessagesImpl.swift +++ b/mobile/ios/Runner/Sync/MessagesImpl.swift @@ -27,7 +27,8 @@ extension PHAsset { updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) }, width: Int64(pixelWidth), height: Int64(pixelHeight), - durationInSeconds: Int64(duration) + durationInSeconds: Int64(duration), + orientation: 0 ) } } @@ -169,7 +170,8 @@ class NativeSyncApiImpl: NativeSyncApi { id: asset.localIdentifier, name: "", type: 0, - durationInSeconds: 0 + durationInSeconds: 0, + orientation: 0 ) if (updatedAssets.contains(AssetWrapper(with: predicate))) { continue diff --git a/mobile/ios/WidgetExtension/EntryGenerators.swift b/mobile/ios/WidgetExtension/EntryGenerators.swift deleted file mode 100644 index 6c1e1d4118..0000000000 --- a/mobile/ios/WidgetExtension/EntryGenerators.swift +++ /dev/null @@ -1,58 +0,0 @@ -import SwiftUI -import WidgetKit - -func buildEntry( - api: ImmichAPI, - asset: SearchResult, - dateOffset: Int, - subtitle: String? = nil -) - async throws -> ImageEntry -{ - let entryDate = Calendar.current.date( - byAdding: .minute, - value: dateOffset * 20, - to: Date.now - )! - let image = try await api.fetchImage(asset: asset) - return ImageEntry(date: entryDate, image: image, subtitle: subtitle) -} - -func generateRandomEntries( - api: ImmichAPI, - now: Date, - count: Int, - albumId: String? = nil, - subtitle: String? = nil -) - async throws -> [ImageEntry] -{ - - var entries: [ImageEntry] = [] - let albumIds = albumId != nil ? [albumId!] : [] - - let randomAssets = try await api.fetchSearchResults( - with: SearchFilters(size: count, albumIds: albumIds) - ) - - await withTaskGroup(of: ImageEntry?.self) { group in - for (dateOffset, asset) in randomAssets.enumerated() { - group.addTask { - return try? await buildEntry( - api: api, - asset: asset, - dateOffset: dateOffset, - subtitle: subtitle - ) - } - } - - for await result in group { - if let entry = result { - entries.append(entry) - } - } - } - - return entries -} diff --git a/mobile/ios/WidgetExtension/ImageEntry.swift b/mobile/ios/WidgetExtension/ImageEntry.swift new file mode 100644 index 0000000000..ee371703a8 --- /dev/null +++ b/mobile/ios/WidgetExtension/ImageEntry.swift @@ -0,0 +1,148 @@ +import SwiftUI +import WidgetKit + +typealias EntryMetadata = ImageEntry.Metadata + +struct ImageEntry: TimelineEntry { + let date: Date + var image: UIImage? + var metadata: Metadata = Metadata() + + struct Metadata: Codable { + var subtitle: String? = nil + var error: WidgetError? = nil + var deepLink: URL? = nil + } + + static func build( + api: ImmichAPI, + asset: Asset, + dateOffset: Int, + subtitle: String? = nil + ) + async throws -> Self + { + let entryDate = Calendar.current.date( + byAdding: .minute, + value: dateOffset * 20, + to: Date.now + )! + let image = try await api.fetchImage(asset: asset) + + return Self( + date: entryDate, + image: image, + metadata: EntryMetadata( + subtitle: subtitle, + deepLink: asset.deepLink + ) + ) + } + + func cache(for key: String) throws { + if let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: IMMICH_SHARE_GROUP + ) { + let imageURL = containerURL.appendingPathComponent("\(key)_image.png") + let metadataURL = containerURL.appendingPathComponent( + "\(key)_metadata.json" + ) + + // build metadata JSON + let entryMetadata = try JSONEncoder().encode(self.metadata) + + // write to disk + try self.image?.pngData()?.write(to: imageURL, options: .atomic) + try entryMetadata.write(to: metadataURL, options: .atomic) + } + } + + static func loadCached(for key: String, at date: Date = Date.now) + -> ImageEntry? + { + if let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: IMMICH_SHARE_GROUP + ) { + let imageURL = containerURL.appendingPathComponent("\(key)_image.png") + let metadataURL = containerURL.appendingPathComponent( + "\(key)_metadata.json" + ) + + guard let imageData = try? Data(contentsOf: imageURL), + let metadataJSON = try? Data(contentsOf: metadataURL), + let decodedMetadata = try? JSONDecoder().decode( + Metadata.self, + from: metadataJSON + ) + else { + return nil + } + + return ImageEntry( + date: date, + image: UIImage(data: imageData), + metadata: decodedMetadata + ) + } + + return nil + } + + static func handleError( + for key: String, + error: WidgetError = .fetchFailed + ) -> Timeline { + var timelineEntry = ImageEntry( + date: Date.now, + image: nil, + metadata: EntryMetadata(error: error) + ) + + // use cache if generic failed error + // we want to show the other errors to the user since without intervention, + // it will never succeed + if error == .fetchFailed, let cachedEntry = ImageEntry.loadCached(for: key) + { + timelineEntry = cachedEntry + } + + return Timeline(entries: [timelineEntry], policy: .atEnd) + } + +} + +func generateRandomEntries( + api: ImmichAPI, + now: Date, + count: Int, + filter: SearchFilter = Album.NONE.filter, + subtitle: String? = nil +) + async throws -> [ImageEntry] +{ + + var entries: [ImageEntry] = [] + + let randomAssets = try await api.fetchSearchResults(with: filter) + + await withTaskGroup(of: ImageEntry?.self) { group in + for (dateOffset, asset) in randomAssets.enumerated() { + group.addTask { + return try? await ImageEntry.build( + api: api, + asset: asset, + dateOffset: dateOffset, + subtitle: subtitle + ) + } + } + + for await result in group { + if let entry = result { + entries.append(entry) + } + } + } + + return entries +} diff --git a/mobile/ios/WidgetExtension/ImageWidgetView.swift b/mobile/ios/WidgetExtension/ImageWidgetView.swift index ff11133e51..8e810b051e 100644 --- a/mobile/ios/WidgetExtension/ImageWidgetView.swift +++ b/mobile/ios/WidgetExtension/ImageWidgetView.swift @@ -1,22 +1,14 @@ import SwiftUI import WidgetKit -struct ImageEntry: TimelineEntry { - let date: Date - var image: UIImage? - var subtitle: String? = nil - var error: WidgetError? = nil - - // Resizes the stored image to a maximum width of 450 pixels - mutating func resize() { - if (image == nil || image!.size.height < 450 || image!.size.width < 450 ) { - return - } - - image = image?.resized(toWidth: 450) - - if image == nil { - error = .unableToResize +extension Image { + @ViewBuilder + func tintedWidgetImageModifier() -> some View { + if #available(iOS 18.0, *) { + self + .widgetAccentedRenderingMode(.accentedDesaturated) + } else { + self } } } @@ -24,27 +16,12 @@ struct ImageEntry: TimelineEntry { struct ImmichWidgetView: View { var entry: ImageEntry - func getErrorText(_ error: WidgetError?) -> String { - switch error { - case .noLogin: - return "Login to Immich" - - case .fetchFailed: - return "Unable to connect to your Immich instance" - - case .albumNotFound: - return "Album not found" - - default: - return "An unknown error occured" - } - } - var body: some View { if entry.image == nil { VStack { Image("LaunchImage") - Text(getErrorText(entry.error)) + .tintedWidgetImageModifier() + Text(entry.metadata.error?.errorDescription ?? "") .minimumScaleFactor(0.25) .multilineTextAlignment(.center) .foregroundStyle(.secondary) @@ -55,11 +32,13 @@ struct ImmichWidgetView: View { Color.clear.overlay( Image(uiImage: entry.image!) .resizable() + .tintedWidgetImageModifier() .scaledToFill() + ) VStack { Spacer() - if let subtitle = entry.subtitle { + if let subtitle = entry.metadata.subtitle { Text(subtitle) .foregroundColor(.white) .padding(8) @@ -70,6 +49,7 @@ struct ImmichWidgetView: View { } .padding(16) } + .widgetURL(entry.metadata.deepLink) } } } @@ -84,7 +64,9 @@ struct ImmichWidgetView: View { ImageEntry( date: date, image: UIImage(named: "ImmichLogo"), - subtitle: "1 year ago" + metadata: EntryMetadata( + subtitle: "1 year ago" + ) ) } ) diff --git a/mobile/ios/WidgetExtension/ImmichAPI.swift b/mobile/ios/WidgetExtension/ImmichAPI.swift index 4da610f1c7..36758b824c 100644 --- a/mobile/ios/WidgetExtension/ImmichAPI.swift +++ b/mobile/ios/WidgetExtension/ImmichAPI.swift @@ -2,12 +2,38 @@ import Foundation import SwiftUI import WidgetKit -enum WidgetError: Error { +let IMMICH_SHARE_GROUP = "group.app.immich.share" + +enum WidgetError: Error, Codable { case noLogin case fetchFailed - case unknown case albumNotFound + case noAssetsAvailable +} + +enum FetchError: Error { case unableToResize + case invalidImage + case invalidURL + case fetchFailed +} + +extension WidgetError: LocalizedError { + public var errorDescription: String? { + switch self { + case .noLogin: + return "Login to Immich" + + case .fetchFailed: + return "Unable to connect to your Immich instance" + + case .albumNotFound: + return "Album not found" + + case .noAssetsAvailable: + return "No assets available" + } + } } enum AssetType: String, Codable { @@ -17,20 +43,25 @@ enum AssetType: String, Codable { case other = "OTHER" } -struct SearchResult: Codable { +struct Asset: Codable { let id: String let type: AssetType + + var deepLink: URL? { + return URL(string: "immich://asset?id=\(id)") + } } -struct SearchFilters: Codable { - var type: AssetType = .image - let size: Int +struct SearchFilter: Codable { + var type = AssetType.image + var size = 1 var albumIds: [String] = [] + var isFavorite: Bool? = nil } struct MemoryResult: Codable { let id: String - var assets: [SearchResult] + var assets: [Asset] let type: String struct MemoryData: Codable { @@ -40,9 +71,34 @@ struct MemoryResult: Codable { let data: MemoryData } -struct Album: Codable { +struct Album: Codable, Equatable { let id: String let albumName: String + + static let NONE = Album(id: "NONE", albumName: "None") + static let FAVORITES = Album(id: "FAVORITES", albumName: "Favorites") + + var filter: SearchFilter { + switch self { + case Album.NONE: + return SearchFilter() + case Album.FAVORITES: + return SearchFilter(isFavorite: true) + + // regular album + default: + return SearchFilter(albumIds: [id]) + } + } + + var isVirtual: Bool { + switch self { + case Album.NONE, Album.FAVORITES: + return true + default: + return false + } + } } // MARK: API @@ -56,7 +112,7 @@ class ImmichAPI { init() async throws { // fetch the credentials from the UserDefaults store that dart placed here - guard let defaults = UserDefaults(suiteName: "group.app.immich.share"), + guard let defaults = UserDefaults(suiteName: IMMICH_SHARE_GROUP), let serverURL = defaults.string(forKey: "widget_server_url"), let sessionKey = defaults.string(forKey: "widget_auth_token") else { @@ -100,8 +156,9 @@ class ImmichAPI { return components?.url } - func fetchSearchResults(with filters: SearchFilters) async throws - -> [SearchResult] + func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter) + async throws + -> [Asset] { // get URL guard @@ -121,7 +178,7 @@ class ImmichAPI { let (data, _) = try await URLSession.shared.data(for: request) // decode data - return try JSONDecoder().decode([SearchResult].self, from: data) + return try JSONDecoder().decode([Asset].self, from: data) } func fetchMemory(for date: Date) async throws -> [MemoryResult] { @@ -146,7 +203,7 @@ class ImmichAPI { return try JSONDecoder().decode([MemoryResult].self, from: data) } - func fetchImage(asset: SearchResult) async throws -> UIImage { + func fetchImage(asset: Asset) async throws(FetchError) -> UIImage { let thumbnailParams = [URLQueryItem(name: "size", value: "preview")] let assetEndpoint = "/assets/" + asset.id + "/thumbnail" @@ -157,16 +214,31 @@ class ImmichAPI { params: thumbnailParams ) else { - throw URLError(.badURL) + throw .invalidURL } - let (data, _) = try await URLSession.shared.data(from: fetchURL) - - guard let img = UIImage(data: data) else { - throw URLError(.badServerResponse) + guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil) + else { + throw .invalidURL } - return img + let decodeOptions: [NSString: Any] = [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceThumbnailMaxPixelSize: 512, + kCGImageSourceCreateThumbnailWithTransform: true, + ] + + guard + let thumbnail = CGImageSourceCreateThumbnailAtIndex( + imageSource, + 0, + decodeOptions as CFDictionary + ) + else { + throw .fetchFailed + } + + return UIImage(cgImage: thumbnail) } func fetchAlbums() async throws -> [Album] { diff --git a/mobile/ios/WidgetExtension/Info.plist b/mobile/ios/WidgetExtension/Info.plist index 0f118fb75e..d4e598ee31 100644 --- a/mobile/ios/WidgetExtension/Info.plist +++ b/mobile/ios/WidgetExtension/Info.plist @@ -2,6 +2,11 @@ + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExtension NSExtensionPointIdentifier diff --git a/mobile/ios/WidgetExtension/UIImage+Resize.swift b/mobile/ios/WidgetExtension/UIImage+Resize.swift index 40bb9e2ace..030f354ca4 100644 --- a/mobile/ios/WidgetExtension/UIImage+Resize.swift +++ b/mobile/ios/WidgetExtension/UIImage+Resize.swift @@ -7,14 +7,17 @@ import UIKit extension UIImage { - /// Crops the image to ensure width and height do not exceed maxSize. - /// Keeps original aspect ratio and crops excess equally from edges (center crop). - func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? { - let canvas = CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height))) - let format = imageRendererFormat - format.opaque = isOpaque - return UIGraphicsImageRenderer(size: canvas, format: format).image { - _ in draw(in: CGRect(origin: .zero, size: canvas)) - } + /// Crops the image to ensure width and height do not exceed maxSize. + /// Keeps original aspect ratio and crops excess equally from edges (center crop). + func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? { + let canvas = CGSize( + width: width, + height: CGFloat(ceil(width / size.width * size.height)) + ) + let format = imageRendererFormat + format.opaque = isOpaque + return UIGraphicsImageRenderer(size: canvas, format: format).image { + _ in draw(in: CGRect(origin: .zero, size: canvas)) } + } } diff --git a/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift b/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift index 516bf6905e..d0a3e8c29d 100644 --- a/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift +++ b/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift @@ -19,28 +19,31 @@ struct ImmichMemoryProvider: TimelineProvider { in context: Context, completion: @escaping @Sendable (ImageEntry) -> Void ) { + let cacheKey = "memory_\(context.family.rawValue)" + Task { guard let api = try? await ImmichAPI() else { - completion(ImageEntry(date: Date(), image: nil, error: .noLogin)) + completion( + ImageEntry.handleError(for: cacheKey, error: .noLogin).entries.first! + ) return } guard let memories = try? await api.fetchMemory(for: Date.now) else { - completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed)) + completion(ImageEntry.handleError(for: cacheKey).entries.first!) return } for memory in memories { if let asset = memory.assets.first(where: { $0.type == .image }), - var entry = try? await buildEntry( + let entry = try? await ImageEntry.build( api: api, asset: asset, dateOffset: 0, subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year) ) { - entry.resize() completion(entry) return } @@ -48,26 +51,17 @@ struct ImmichMemoryProvider: TimelineProvider { // fallback to random image guard - let randomImage = try? await api.fetchSearchResults( - with: SearchFilters(size: 1) - ).first - else { - completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed)) - return - } - - guard - var imageEntry = try? await buildEntry( + let randomImage = try? await api.fetchSearchResults().first, + let imageEntry = try? await ImageEntry.build( api: api, asset: randomImage, dateOffset: 0 ) else { - completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed)) + completion(ImageEntry.handleError(for: cacheKey).entries.first!) return } - imageEntry.resize() completion(imageEntry) } } @@ -80,9 +74,12 @@ struct ImmichMemoryProvider: TimelineProvider { var entries: [ImageEntry] = [] let now = Date() + let cacheKey = "memory_\(context.family.rawValue)" + guard let api = try? await ImmichAPI() else { - entries.append(ImageEntry(date: now, image: nil, error: .noLogin)) - completion(Timeline(entries: entries, policy: .atEnd)) + completion( + ImageEntry.handleError(for: cacheKey, error: .noLogin) + ) return } @@ -95,7 +92,7 @@ struct ImmichMemoryProvider: TimelineProvider { for asset in memory.assets { if asset.type == .image && totalAssets < 12 { group.addTask { - try? await buildEntry( + try? await ImageEntry.build( api: api, asset: asset, dateOffset: totalAssets, @@ -120,25 +117,32 @@ struct ImmichMemoryProvider: TimelineProvider { // If we didnt add any memory images (some failure occured or no images in memory), // default to 12 hours of random photos if entries.count == 0 { - entries.append( - contentsOf: (try? await generateRandomEntries( + // this must be a do/catch since we need to + // distinguish between a network fail and an empty search + do { + let search = try await generateRandomEntries( api: api, now: now, count: 12 - )) ?? [] - ) + ) + + // Load or save a cached asset for when network conditions are bad + if search.count == 0 { + completion( + ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable) + ) + return + } + + entries.append(contentsOf: search) + } catch { + completion(ImageEntry.handleError(for: cacheKey)) + return + } } - // If we fail to fetch images, we still want to add an entry - // with a nil image and an error - if entries.count == 0 { - entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed)) - } - - // Resize all images to something that can be stored by iOS - for i in entries.indices { - entries[i].resize() - } + // cache the last image + try? entries.last!.cache(for: cacheKey) completion(Timeline(entries: entries, policy: .atEnd)) } diff --git a/mobile/ios/WidgetExtension/widgets/RandomWidget.swift b/mobile/ios/WidgetExtension/widgets/RandomWidget.swift index 99968c4baa..37f3c5e596 100644 --- a/mobile/ios/WidgetExtension/widgets/RandomWidget.swift +++ b/mobile/ios/WidgetExtension/widgets/RandomWidget.swift @@ -8,20 +8,21 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable { struct AlbumQuery: EntityQuery { func entities(for identifiers: [Album.ID]) async throws -> [Album] { - // use cached albums to search - var albums = (try? await AlbumCache.shared.getAlbums()) ?? [] - albums.insert(NO_ALBUM, at: 0) - - return albums.filter { + return await suggestedEntities().filter { identifiers.contains($0.id) } } - func suggestedEntities() async throws -> [Album] { - var albums = (try? await AlbumCache.shared.getAlbums(refresh: true)) ?? [] - albums.insert(NO_ALBUM, at: 0) + func suggestedEntities() async -> [Album] { + let albums = (try? await AlbumCache.shared.getAlbums()) ?? [] - return albums + let options = + [ + NONE, + FAVORITES, + ] + albums + + return options } } @@ -35,8 +36,6 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable { } } -let NO_ALBUM = Album(id: "NONE", albumName: "None") - struct RandomConfigurationAppIntent: WidgetConfigurationIntent { static var title: LocalizedStringResource { "Select Album" } static var description: IntentDescription { @@ -45,7 +44,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent { @Parameter(title: "Album") var album: Album? - + @Parameter(title: "Show Album Name", default: false) var showAlbumName: Bool } @@ -54,7 +53,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent { struct ImmichRandomProvider: AppIntentTimelineProvider { func placeholder(in context: Context) -> ImageEntry { - ImageEntry(date: Date(), image: nil) + ImageEntry(date: Date()) } func snapshot( @@ -63,30 +62,26 @@ struct ImmichRandomProvider: AppIntentTimelineProvider { ) async -> ImageEntry { + let cacheKey = "random_none_\(context.family.rawValue)" + guard let api = try? await ImmichAPI() else { - return ImageEntry(date: Date(), image: nil, error: .noLogin) + return ImageEntry.handleError(for: cacheKey, error: .noLogin).entries + .first! } guard let randomImage = try? await api.fetchSearchResults( - with: SearchFilters(size: 1) - ).first - else { - return ImageEntry(date: Date(), image: nil, error: .fetchFailed) - } - - guard - var entry = try? await buildEntry( + with: Album.NONE.filter + ).first, + let entry = try? await ImageEntry.build( api: api, asset: randomImage, dateOffset: 0 ) else { - return ImageEntry(date: Date(), image: nil, error: .fetchFailed) + return ImageEntry.handleError(for: cacheKey).entries.first! } - entry.resize() - return entry } @@ -99,50 +94,41 @@ struct ImmichRandomProvider: AppIntentTimelineProvider { var entries: [ImageEntry] = [] let now = Date() + // nil if album is NONE or nil + let album = configuration.album ?? Album.NONE + let albumName = album.isVirtual ? nil : album.albumName + + let cacheKey = "random_\(album.id)_\(context.family.rawValue)" + // If we don't have a server config, return an entry with an error guard let api = try? await ImmichAPI() else { - entries.append(ImageEntry(date: now, image: nil, error: .noLogin)) - return Timeline(entries: entries, policy: .atEnd) + return ImageEntry.handleError(for: cacheKey, error: .noLogin) } - // nil if album is NONE or nil - let albumId = - configuration.album?.id != "NONE" ? configuration.album?.id : nil - var albumName: String? = albumId != nil ? configuration.album?.albumName : nil - - if albumId != nil { - // make sure the album exists on server, otherwise show error - guard let albums = try? await api.fetchAlbums() else { - entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed)) - return Timeline(entries: entries, policy: .atEnd) - } - - if !albums.contains(where: { $0.id == albumId }) { - entries.append(ImageEntry(date: now, image: nil, error: .albumNotFound)) - return Timeline(entries: entries, policy: .atEnd) - } - } - - entries.append( - contentsOf: (try? await generateRandomEntries( + // build entries + // this must be a do/catch since we need to + // distinguish between a network fail and an empty search + do { + let search = try await generateRandomEntries( api: api, now: now, count: 12, - albumId: albumId, + filter: album.filter, subtitle: configuration.showAlbumName ? albumName : nil - )) - ?? [] - ) + ) - // If we fail to fetch images, we still want to add an entry with a nil image and an error - if entries.count == 0 { - entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed)) + // Load or save a cached asset for when network conditions are bad + if search.count == 0 { + return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable) + } + + entries.append(contentsOf: search) + } catch { + return ImageEntry.handleError(for: cacheKey) } - // Resize all images to something that can be stored by iOS - for i in entries.indices { - entries[i].resize() - } + // cache the last image + try? entries.last!.cache(for: cacheKey) return Timeline(entries: entries, policy: .atEnd) } diff --git a/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json b/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json deleted file mode 100644 index 7391713b6f..0000000000 --- a/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":[""]},"commands":{"":{"tool":"phony","inputs":[""],"outputs":[""]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":[""]}}} \ No newline at end of file diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 4c60a2e831..70fade71ba 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -22,7 +22,7 @@ platform :ios do path: "./Runner.xcodeproj", ) increment_version_number( - version_number: "1.135.3" + version_number: "1.136.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/lib/constants/colors.dart b/mobile/lib/constants/colors.dart index ade878d6f6..1614a308e0 100644 --- a/mobile/lib/constants/colors.dart +++ b/mobile/lib/constants/colors.dart @@ -10,7 +10,7 @@ enum ImmichColorPreset { lime, green, cyan, - slateGray + slateGray, } const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo; diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 6d98152efc..b3d9d138c4 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -10,12 +10,20 @@ const int kSyncEventBatchSize = 5000; const int kFetchLocalAssetsBatchSize = 40000; // Hash batch limits -const int kBatchHashFileLimit = 128; +const int kBatchHashFileLimit = 256; const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB // Secure storage keys const String kSecuredPinCode = "secured_pin_code"; +// background_downloader task groups +const String kManualUploadGroup = 'manual_upload_group'; +const String kBackupGroup = 'backup_group'; +const String kBackupLivePhotoGroup = 'backup_live_photo_group'; +const String kDownloadGroupImage = 'group_image'; +const String kDownloadGroupVideo = 'group_video'; +const String kDownloadGroupLivePhoto = 'group_livephoto'; + // Timeline constants const int kTimelineNoneSegmentSize = 120; const int kTimelineAssetLoadBatchSize = 256; @@ -28,7 +36,11 @@ const String appShareGroupId = "group.app.immich.share"; // add widget identifiers here for new widgets // these are used to force a widget refresh -const List kWidgetNames = [ - 'com.immich.widget.random', - 'com.immich.widget.memory', +// (iOSName, androidFQDN) +const List<(String, String)> kWidgetNames = [ + ('com.immich.widget.random', 'app.alextran.immich.widget.RandomReceiver'), + ('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'), ]; + +const double kUploadStatusFailed = -1.0; +const double kUploadStatusCanceled = -2.0; diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index 4999c48660..febc71032e 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -12,3 +12,5 @@ enum TextSearchType { enum AssetVisibilityEnum { timeline, hidden, archive, locked } enum SortUserBy { id } + +enum ActionSource { timeline, viewer } diff --git a/mobile/lib/constants/errors.dart b/mobile/lib/constants/errors.dart index 3d1f775033..0ee1195737 100644 --- a/mobile/lib/constants/errors.dart +++ b/mobile/lib/constants/errors.dart @@ -4,6 +4,8 @@ sealed class ImmichErrors { } class NoResponseDtoError extends ImmichErrors implements Exception { + const NoResponseDtoError(); + @override String toString() => "Response Dto is null"; } diff --git a/mobile/lib/constants/locales.dart b/mobile/lib/constants/locales.dart index 658242ea3a..601a83b563 100644 --- a/mobile/lib/constants/locales.dart +++ b/mobile/lib/constants/locales.dart @@ -7,10 +7,9 @@ const Map locales = { 'Arabic (ar)': Locale('ar'), 'Bulgarian (bg)': Locale('bg'), 'Catalan (ca)': Locale('ca'), - 'Chinese Simplified (zh_CN)': - Locale.fromSubtags(languageCode: 'zh', scriptCode: 'SIMPLIFIED'), - 'Chinese Traditional (zh_TW)': - Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), + 'Chinese Simplified (zh_CN)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'SIMPLIFIED'), + 'Chinese Traditional (zh_TW)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), + 'Croatian (hr)': Locale('hr'), 'Czech (cs)': Locale('cs'), 'Danish (da)': Locale('da'), 'Dutch (nl)': Locale('nl'), @@ -36,10 +35,8 @@ const Map locales = { 'Portuguese (pt)': Locale('pt'), 'Romanian (ro)': Locale('ro'), 'Russian (ru)': Locale('ru'), - 'Serbian Cyrillic (sr_Cyrl)': - Locale.fromSubtags(languageCode: 'sr', scriptCode: 'Cyrl'), - 'Serbian Latin (sr_Latn)': - Locale.fromSubtags(languageCode: 'sr', scriptCode: 'Latn'), + 'Serbian Cyrillic (sr_Cyrl)': Locale.fromSubtags(languageCode: 'sr', scriptCode: 'Cyrl'), + 'Serbian Latin (sr_Latn)': Locale.fromSubtags(languageCode: 'sr', scriptCode: 'Latn'), 'Slovak (sk)': Locale('sk'), 'Slovenian (sl)': Locale('sl'), 'Spanish (es)': Locale('es'), diff --git a/mobile/lib/domain/interfaces/asset_media.interface.dart b/mobile/lib/domain/interfaces/asset_media.interface.dart deleted file mode 100644 index 93f99827ee..0000000000 --- a/mobile/lib/domain/interfaces/asset_media.interface.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'dart:typed_data'; -import 'dart:ui'; - -abstract interface class IAssetMediaRepository { - Future getThumbnail( - String id, { - int quality = 80, - Size size = const Size.square(256), - }); -} diff --git a/mobile/lib/domain/interfaces/device_asset.interface.dart b/mobile/lib/domain/interfaces/device_asset.interface.dart deleted file mode 100644 index 1df8cc2250..0000000000 --- a/mobile/lib/domain/interfaces/device_asset.interface.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:async'; - -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; -import 'package:immich_mobile/domain/models/device_asset.model.dart'; - -abstract interface class IDeviceAssetRepository implements IDatabaseRepository { - Future updateAll(List assetHash); - - Future> getByIds(List localIds); - - Future deleteIds(List ids); -} diff --git a/mobile/lib/domain/interfaces/exif.interface.dart b/mobile/lib/domain/interfaces/exif.interface.dart deleted file mode 100644 index a5de6167e9..0000000000 --- a/mobile/lib/domain/interfaces/exif.interface.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; - -abstract interface class IExifInfoRepository implements IDatabaseRepository { - Future get(int assetId); - - Future update(ExifInfo exifInfo); - - Future> updateAll(List exifInfos); - - Future delete(int assetId); - - Future deleteAll(); -} diff --git a/mobile/lib/domain/interfaces/local_album.interface.dart b/mobile/lib/domain/interfaces/local_album.interface.dart deleted file mode 100644 index 1df62954d2..0000000000 --- a/mobile/lib/domain/interfaces/local_album.interface.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/local_album.model.dart'; - -abstract interface class ILocalAlbumRepository implements IDatabaseRepository { - Future> getAll({Set sortBy = const {}}); - - Future> getAssets(String albumId); - - Future> getAssetIds(String albumId); - - Future upsert( - LocalAlbum album, { - Iterable toUpsert = const [], - Iterable toDelete = const [], - }); - - Future updateAll(Iterable albums); - - Future delete(String albumId); - - Future processDelta({ - required List updates, - required List deletes, - required Map> assetAlbums, - }); - - Future syncDeletes(String albumId, Iterable assetIdsToKeep); - - Future> getAssetsToHash(String albumId); -} - -enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum } diff --git a/mobile/lib/domain/interfaces/local_asset.interface.dart b/mobile/lib/domain/interfaces/local_asset.interface.dart deleted file mode 100644 index 5792ebe5d9..0000000000 --- a/mobile/lib/domain/interfaces/local_asset.interface.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; - -abstract interface class ILocalAssetRepository implements IDatabaseRepository { - Future updateHashes(Iterable hashes); -} diff --git a/mobile/lib/domain/interfaces/log.interface.dart b/mobile/lib/domain/interfaces/log.interface.dart deleted file mode 100644 index 27e91c5488..0000000000 --- a/mobile/lib/domain/interfaces/log.interface.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:async'; - -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; -import 'package:immich_mobile/domain/models/log.model.dart'; - -abstract interface class ILogRepository implements IDatabaseRepository { - Future insert(LogMessage log); - - Future insertAll(Iterable logs); - - Future> getAll(); - - Future deleteAll(); - - /// Truncates the logs to the most recent [limit]. Defaults to recent 250 logs - Future truncate({int limit = 250}); -} diff --git a/mobile/lib/domain/interfaces/storage.interface.dart b/mobile/lib/domain/interfaces/storage.interface.dart deleted file mode 100644 index ea6513e7f2..0000000000 --- a/mobile/lib/domain/interfaces/storage.interface.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'dart:io'; - -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; - -abstract interface class IStorageRepository { - Future getFileForAsset(LocalAsset asset); -} diff --git a/mobile/lib/domain/interfaces/store.interface.dart b/mobile/lib/domain/interfaces/store.interface.dart deleted file mode 100644 index b0a6762566..0000000000 --- a/mobile/lib/domain/interfaces/store.interface.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; - -abstract interface class IStoreRepository implements IDatabaseRepository { - Future insert(StoreKey key, T value); - - Future tryGet(StoreKey key); - - Future>> getAll(); - - Stream watch(StoreKey key); - - Stream> watchAll(); - - Future update(StoreKey key, T value); - - Future delete(StoreKey key); - - Future deleteAll(); -} diff --git a/mobile/lib/domain/interfaces/sync_api.interface.dart b/mobile/lib/domain/interfaces/sync_api.interface.dart deleted file mode 100644 index 57abed2e7f..0000000000 --- a/mobile/lib/domain/interfaces/sync_api.interface.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:http/http.dart' as http; -import 'package:immich_mobile/domain/models/sync_event.model.dart'; - -abstract interface class ISyncApiRepository { - Future ack(List data); - - Future streamChanges( - Function(List, Function() abort) onData, { - int batchSize, - http.Client? httpClient, - }); -} diff --git a/mobile/lib/domain/interfaces/timeline.interface.dart b/mobile/lib/domain/interfaces/timeline.interface.dart deleted file mode 100644 index e60dd83b50..0000000000 --- a/mobile/lib/domain/interfaces/timeline.interface.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/timeline.model.dart'; - -abstract interface class ITimelineRepository implements IDatabaseRepository { - Stream> watchMainBucket( - List timelineUsers, { - GroupAssetsBy groupBy = GroupAssetsBy.day, - }); - - Future> getMainBucketAssets( - List timelineUsers, { - required int offset, - required int count, - }); - - Stream> watchLocalBucket( - String albumId, { - GroupAssetsBy groupBy = GroupAssetsBy.day, - }); - - Future> getLocalBucketAssets( - String albumId, { - required int offset, - required int count, - }); -} diff --git a/mobile/lib/domain/models/album/album.model.dart b/mobile/lib/domain/models/album/album.model.dart new file mode 100644 index 0000000000..a199bce129 --- /dev/null +++ b/mobile/lib/domain/models/album/album.model.dart @@ -0,0 +1,117 @@ +enum AlbumAssetOrder { + // do not change this order! + asc, + desc, +} + +enum AlbumUserRole { + // do not change this order! + editor, + viewer, +} + +// Model for an album stored in the server +class RemoteAlbum { + final String id; + final String name; + final String ownerId; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final AlbumAssetOrder order; + final int assetCount; + final String ownerName; + + const RemoteAlbum({ + required this.id, + required this.name, + required this.ownerId, + required this.description, + required this.createdAt, + required this.updatedAt, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + required this.assetCount, + required this.ownerName, + }); + + @override + String toString() { + return '''Album { + id: $id, + name: $name, + ownerId: $ownerId, + description: $description, + createdAt: $createdAt, + updatedAt: $updatedAt, + isActivityEnabled: $isActivityEnabled, + order: $order, + thumbnailAssetId: ${thumbnailAssetId ?? ""} + assetCount: $assetCount + ownerName: $ownerName + }'''; + } + + @override + bool operator ==(Object other) { + if (other is! RemoteAlbum) return false; + if (identical(this, other)) return true; + return id == other.id && + name == other.name && + ownerId == other.ownerId && + description == other.description && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + thumbnailAssetId == other.thumbnailAssetId && + isActivityEnabled == other.isActivityEnabled && + order == other.order && + assetCount == other.assetCount && + ownerName == other.ownerName; + } + + @override + int get hashCode { + return id.hashCode ^ + name.hashCode ^ + ownerId.hashCode ^ + description.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + thumbnailAssetId.hashCode ^ + isActivityEnabled.hashCode ^ + order.hashCode ^ + assetCount.hashCode ^ + ownerName.hashCode; + } + + RemoteAlbum copyWith({ + String? id, + String? name, + String? ownerId, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? thumbnailAssetId, + bool? isActivityEnabled, + AlbumAssetOrder? order, + int? assetCount, + String? ownerName, + }) { + return RemoteAlbum( + id: id ?? this.id, + name: name ?? this.name, + ownerId: ownerId ?? this.ownerId, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + assetCount: assetCount ?? this.assetCount, + ownerName: ownerName ?? this.ownerName, + ); + } +} diff --git a/mobile/lib/domain/models/local_album.model.dart b/mobile/lib/domain/models/album/local_album.model.dart similarity index 100% rename from mobile/lib/domain/models/local_album.model.dart rename to mobile/lib/domain/models/album/local_album.model.dart diff --git a/mobile/lib/domain/models/asset/asset.model.dart b/mobile/lib/domain/models/asset/asset.model.dart deleted file mode 100644 index b0a0b0dfb9..0000000000 --- a/mobile/lib/domain/models/asset/asset.model.dart +++ /dev/null @@ -1,73 +0,0 @@ -part of 'base_asset.model.dart'; - -enum AssetVisibility { - timeline, - hidden, - archive, - locked, -} - -// Model for an asset stored in the server -class Asset extends BaseAsset { - final String id; - final String? localId; - final String? thumbHash; - final AssetVisibility visibility; - - const Asset({ - required this.id, - this.localId, - required super.name, - required super.checksum, - required super.type, - required super.createdAt, - required super.updatedAt, - super.width, - super.height, - super.durationInSeconds, - super.isFavorite = false, - this.thumbHash, - this.visibility = AssetVisibility.timeline, - }); - - @override - AssetState get storage => - localId == null ? AssetState.remote : AssetState.merged; - - @override - String toString() { - return '''Asset { - id: $id, - name: $name, - type: $type, - createdAt: $createdAt, - updatedAt: $updatedAt, - width: ${width ?? ""}, - height: ${height ?? ""}, - durationInSeconds: ${durationInSeconds ?? ""}, - localId: ${localId ?? ""}, - isFavorite: $isFavorite, - thumbHash: ${thumbHash ?? ""}, - visibility: $visibility, - }'''; - } - - @override - bool operator ==(Object other) { - if (other is! Asset) return false; - if (identical(this, other)) return true; - return super == other && - id == other.id && - localId == other.localId && - thumbHash == other.thumbHash && - visibility == other.visibility; - } - - @override - int get hashCode => - super.hashCode ^ - id.hashCode ^ - localId.hashCode ^ - thumbHash.hashCode ^ - visibility.hashCode; -} diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index 509998a109..7cd4caab6a 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -1,5 +1,5 @@ -part 'asset.model.dart'; part 'local_asset.model.dart'; +part 'remote_asset.model.dart'; enum AssetType { // do not change this order! @@ -25,6 +25,7 @@ sealed class BaseAsset { final int? height; final int? durationInSeconds; final bool isFavorite; + final String? livePhotoVideoId; const BaseAsset({ required this.name, @@ -36,11 +37,30 @@ sealed class BaseAsset { this.height, this.durationInSeconds, this.isFavorite = false, + this.livePhotoVideoId, }); bool get isImage => type == AssetType.image; bool get isVideo => type == AssetType.video; + + bool get isMotionPhoto => livePhotoVideoId != null; + + Duration get duration { + final durationInSeconds = this.durationInSeconds; + if (durationInSeconds != null) { + return Duration(seconds: durationInSeconds); + } + return const Duration(); + } + + bool get hasRemote => storage == AssetState.remote || storage == AssetState.merged; + bool get hasLocal => storage == AssetState.local || storage == AssetState.merged; + bool get isLocalOnly => storage == AssetState.local; + bool get isRemoteOnly => storage == AssetState.remote; + + // Overridden in subclasses AssetState get storage; + String get heroTag; @override String toString() { diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart index 95eb1bce9f..9cd20acb0a 100644 --- a/mobile/lib/domain/models/asset/local_asset.model.dart +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -3,6 +3,7 @@ part of 'base_asset.model.dart'; class LocalAsset extends BaseAsset { final String id; final String? remoteId; + final int orientation; const LocalAsset({ required this.id, @@ -16,11 +17,15 @@ class LocalAsset extends BaseAsset { super.height, super.durationInSeconds, super.isFavorite = false, + super.livePhotoVideoId, + this.orientation = 0, }); @override - AssetState get storage => - remoteId == null ? AssetState.local : AssetState.merged; + AssetState get storage => remoteId == null ? AssetState.local : AssetState.merged; + + @override + String get heroTag => '${id}_${remoteId ?? checksum}'; @override String toString() { @@ -35,18 +40,20 @@ class LocalAsset extends BaseAsset { durationInSeconds: ${durationInSeconds ?? ""}, remoteId: ${remoteId ?? ""} isFavorite: $isFavorite, + orientation: $orientation, }'''; } + // Not checking for remoteId here @override bool operator ==(Object other) { if (other is! LocalAsset) return false; if (identical(this, other)) return true; - return super == other && id == other.id && remoteId == other.remoteId; + return super == other && id == other.id && orientation == other.orientation; } @override - int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode; + int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode; LocalAsset copyWith({ String? id, @@ -60,6 +67,7 @@ class LocalAsset extends BaseAsset { int? height, int? durationInSeconds, bool? isFavorite, + int? orientation, }) { return LocalAsset( id: id ?? this.id, @@ -73,6 +81,7 @@ class LocalAsset extends BaseAsset { height: height ?? this.height, durationInSeconds: durationInSeconds ?? this.durationInSeconds, isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, ); } } diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart new file mode 100644 index 0000000000..db3e53cd2e --- /dev/null +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -0,0 +1,126 @@ +part of 'base_asset.model.dart'; + +enum AssetVisibility { + timeline, + hidden, + archive, + locked, +} + +// Model for an asset stored in the server +class RemoteAsset extends BaseAsset { + final String id; + final String? localId; + final String? thumbHash; + final AssetVisibility visibility; + final String ownerId; + final String? stackId; + + const RemoteAsset({ + required this.id, + this.localId, + required super.name, + required this.ownerId, + required super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + super.isFavorite = false, + this.thumbHash, + this.visibility = AssetVisibility.timeline, + super.livePhotoVideoId, + this.stackId, + }); + + @override + AssetState get storage => localId == null ? AssetState.remote : AssetState.merged; + + @override + String get heroTag => '${localId ?? checksum}_$id'; + + @override + String toString() { + return '''Asset { + id: $id, + name: $name, + ownerId: $ownerId, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""}, + localId: ${localId ?? ""}, + isFavorite: $isFavorite, + thumbHash: ${thumbHash ?? ""}, + visibility: $visibility, + stackId: ${stackId ?? ""}, + checksum: $checksum, + livePhotoVideoId: ${livePhotoVideoId ?? ""}, + }'''; + } + + // Not checking for localId here + @override + bool operator ==(Object other) { + if (other is! RemoteAsset) return false; + if (identical(this, other)) return true; + return super == other && + id == other.id && + ownerId == other.ownerId && + thumbHash == other.thumbHash && + visibility == other.visibility && + stackId == other.stackId; + } + + @override + int get hashCode => + super.hashCode ^ + id.hashCode ^ + ownerId.hashCode ^ + localId.hashCode ^ + thumbHash.hashCode ^ + visibility.hashCode ^ + stackId.hashCode; + + RemoteAsset 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, + }) { + return RemoteAsset( + 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, + ); + } +} diff --git a/mobile/lib/domain/models/asset_face.model.dart b/mobile/lib/domain/models/asset_face.model.dart new file mode 100644 index 0000000000..f432b923e3 --- /dev/null +++ b/mobile/lib/domain/models/asset_face.model.dart @@ -0,0 +1,98 @@ +// Model for an asset face stored in the server +class AssetFace { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + + const AssetFace({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + + AssetFace copyWith({ + String? id, + String? assetId, + String? personId, + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) { + return AssetFace( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + String toString() { + return '''AssetFace { + id: $id, + assetId: $assetId, + personId: ${personId ?? ""}, + imageWidth: $imageWidth, + imageHeight: $imageHeight, + boundingBoxX1: $boundingBoxX1, + boundingBoxY1: $boundingBoxY1, + boundingBoxX2: $boundingBoxX2, + boundingBoxY2: $boundingBoxY2, + sourceType: $sourceType, +}'''; + } + + @override + bool operator ==(covariant AssetFace other) { + if (identical(this, other)) return true; + + return other.id == id && + other.assetId == assetId && + other.personId == personId && + other.imageWidth == imageWidth && + other.imageHeight == imageHeight && + other.boundingBoxX1 == boundingBoxX1 && + other.boundingBoxY1 == boundingBoxY1 && + other.boundingBoxX2 == boundingBoxX2 && + other.boundingBoxY2 == boundingBoxY2 && + other.sourceType == sourceType; + } + + @override + int get hashCode { + return id.hashCode ^ + assetId.hashCode ^ + personId.hashCode ^ + imageWidth.hashCode ^ + imageHeight.hashCode ^ + boundingBoxX1.hashCode ^ + boundingBoxY1.hashCode ^ + boundingBoxX2.hashCode ^ + boundingBoxY2.hashCode ^ + sourceType.hashCode; + } +} diff --git a/mobile/lib/domain/models/device_asset.model.dart b/mobile/lib/domain/models/device_asset.model.dart index 2ec56b0d80..b0949ccc96 100644 --- a/mobile/lib/domain/models/device_asset.model.dart +++ b/mobile/lib/domain/models/device_asset.model.dart @@ -15,9 +15,7 @@ class DeviceAsset { bool operator ==(covariant DeviceAsset other) { if (identical(this, other)) return true; - return other.assetId == assetId && - other.hash == hash && - other.modifiedTime == modifiedTime; + return other.assetId == assetId && other.hash == hash && other.modifiedTime == modifiedTime; } @override diff --git a/mobile/lib/domain/models/exif.model.dart b/mobile/lib/domain/models/exif.model.dart index e95653ca4e..6e94c44650 100644 --- a/mobile/lib/domain/models/exif.model.dart +++ b/mobile/lib/domain/models/exif.model.dart @@ -3,6 +3,8 @@ class ExifInfo { final int? fileSize; final String? description; final bool isFlipped; + final double? width; + final double? height; final String? orientation; final String? timeZone; final DateTime? dateTimeOriginal; @@ -23,8 +25,7 @@ class ExifInfo { final int? iso; final double? exposureSeconds; - bool get hasCoordinates => - latitude != null && longitude != null && latitude != 0 && longitude != 0; + bool get hasCoordinates => latitude != null && longitude != null && latitude != 0 && longitude != 0; String get exposureTime { if (exposureSeconds == null) { @@ -45,6 +46,8 @@ class ExifInfo { this.fileSize, this.description, this.orientation, + this.width, + this.height, this.timeZone, this.dateTimeOriginal, this.isFlipped = false, @@ -68,6 +71,9 @@ class ExifInfo { return other.fileSize == fileSize && other.description == description && + other.isFlipped == isFlipped && + other.width == width && + other.height == height && other.orientation == orientation && other.timeZone == timeZone && other.dateTimeOriginal == dateTimeOriginal && @@ -91,6 +97,9 @@ class ExifInfo { return fileSize.hashCode ^ description.hashCode ^ orientation.hashCode ^ + isFlipped.hashCode ^ + width.hashCode ^ + height.hashCode ^ timeZone.hashCode ^ dateTimeOriginal.hashCode ^ latitude.hashCode ^ @@ -114,6 +123,9 @@ class ExifInfo { fileSize: ${fileSize ?? 'NA'}, description: ${description ?? 'NA'}, orientation: ${orientation ?? 'NA'}, +width: ${width ?? 'NA'}, +height: ${height ?? 'NA'}, +isFlipped: $isFlipped, timeZone: ${timeZone ?? 'NA'}, dateTimeOriginal: ${dateTimeOriginal ?? 'NA'}, latitude: ${latitude ?? 'NA'}, diff --git a/mobile/lib/domain/models/log.model.dart b/mobile/lib/domain/models/log.model.dart index dffd1cccda..f58cae8063 100644 --- a/mobile/lib/domain/models/log.model.dart +++ b/mobile/lib/domain/models/log.model.dart @@ -43,12 +43,7 @@ class LogMessage { @override int get hashCode { - return message.hashCode ^ - level.hashCode ^ - createdAt.hashCode ^ - logger.hashCode ^ - error.hashCode ^ - stack.hashCode; + return message.hashCode ^ level.hashCode ^ createdAt.hashCode ^ logger.hashCode ^ error.hashCode ^ stack.hashCode; } @override diff --git a/mobile/lib/domain/models/memory.model.dart b/mobile/lib/domain/models/memory.model.dart new file mode 100644 index 0000000000..39a6d4518b --- /dev/null +++ b/mobile/lib/domain/models/memory.model.dart @@ -0,0 +1,179 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +import 'package:collection/collection.dart'; + +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; + +enum MemoryTypeEnum { + // do not change this order! + onThisDay, +} + +class MemoryData { + final int year; + + const MemoryData({ + required this.year, + }); + + MemoryData copyWith({ + int? year, + }) { + return MemoryData( + year: year ?? this.year, + ); + } + + Map toMap() { + return { + 'year': year, + }; + } + + factory MemoryData.fromMap(Map map) { + return MemoryData( + year: map['year'] as int, + ); + } + + String toJson() => json.encode(toMap()); + + factory MemoryData.fromJson(String source) => MemoryData.fromMap(json.decode(source) as Map); + + @override + String toString() => 'MemoryData(year: $year)'; + + @override + bool operator ==(covariant MemoryData other) { + if (identical(this, other)) return true; + + return other.year == year; + } + + @override + int get hashCode => year.hashCode; +} + +// Model for a memory stored in the server +class DriftMemory { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + + // enum + final MemoryTypeEnum type; + final MemoryData data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + final List assets; + + const DriftMemory({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + required this.assets, + }); + + DriftMemory copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + DateTime? deletedAt, + String? ownerId, + MemoryTypeEnum? type, + MemoryData? data, + bool? isSaved, + DateTime? memoryAt, + DateTime? seenAt, + DateTime? showAt, + DateTime? hideAt, + List? assets, + }) { + return DriftMemory( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + assets: assets ?? this.assets, + ); + } + + @override + String toString() { + return '''Memory { + id: $id, + createdAt: $createdAt, + updatedAt: $updatedAt, + deletedAt: ${deletedAt ?? ""}, + ownerId: $ownerId, + type: $type, + data: $data, + isSaved: $isSaved, + memoryAt: $memoryAt, + seenAt: ${seenAt ?? ""}, + showAt: ${showAt ?? ""}, + hideAt: ${hideAt ?? ""}, + assets: $assets +}'''; + } + + @override + bool operator ==(covariant DriftMemory other) { + if (identical(this, other)) return true; + final listEquals = const DeepCollectionEquality().equals; + + return other.id == id && + other.createdAt == createdAt && + other.updatedAt == updatedAt && + other.deletedAt == deletedAt && + other.ownerId == ownerId && + other.type == type && + other.data == data && + other.isSaved == isSaved && + other.memoryAt == memoryAt && + other.seenAt == seenAt && + other.showAt == showAt && + other.hideAt == hideAt && + listEquals(other.assets, assets); + } + + @override + int get hashCode { + return id.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + deletedAt.hashCode ^ + ownerId.hashCode ^ + type.hashCode ^ + data.hashCode ^ + isSaved.hashCode ^ + memoryAt.hashCode ^ + seenAt.hashCode ^ + showAt.hashCode ^ + hideAt.hashCode ^ + assets.hashCode; + } +} diff --git a/mobile/lib/domain/models/person.model.dart b/mobile/lib/domain/models/person.model.dart index 63b9f5c159..784bb564fe 100644 --- a/mobile/lib/domain/models/person.model.dart +++ b/mobile/lib/domain/models/person.model.dart @@ -1,7 +1,8 @@ import 'dart:convert'; -class Person { - Person({ +// TODO: Remove PersonDto once Isar is removed +class PersonDto { + const PersonDto({ required this.id, this.birthDate, required this.isHidden, @@ -22,7 +23,7 @@ class Person { return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)'; } - Person copyWith({ + PersonDto copyWith({ String? id, DateTime? birthDate, bool? isHidden, @@ -30,7 +31,7 @@ class Person { String? thumbnailPath, DateTime? updatedAt, }) { - return Person( + return PersonDto( id: id ?? this.id, birthDate: birthDate ?? this.birthDate, isHidden: isHidden ?? this.isHidden, @@ -51,28 +52,23 @@ class Person { }; } - factory Person.fromMap(Map map) { - return Person( + factory PersonDto.fromMap(Map map) { + return PersonDto( id: map['id'] as String, - birthDate: map['birthDate'] != null - ? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int) - : null, + birthDate: map['birthDate'] != null ? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int) : null, isHidden: map['isHidden'] as bool, name: map['name'] as String, thumbnailPath: map['thumbnailPath'] as String, - updatedAt: map['updatedAt'] != null - ? DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int) - : null, + updatedAt: map['updatedAt'] != null ? DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int) : null, ); } String toJson() => json.encode(toMap()); - factory Person.fromJson(String source) => - Person.fromMap(json.decode(source) as Map); + factory PersonDto.fromJson(String source) => PersonDto.fromMap(json.decode(source) as Map); @override - bool operator ==(covariant Person other) { + bool operator ==(covariant PersonDto other) { if (identical(this, other)) return true; return other.id == id && @@ -93,3 +89,102 @@ class Person { updatedAt.hashCode; } } + +// Model for a person stored in the server +class Person { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + + const Person({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + required this.color, + this.birthDate, + }); + + Person copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + String? faceAssetId, + bool? isFavorite, + bool? isHidden, + String? color, + DateTime? birthDate, + }) { + return Person( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + String toString() { + return '''Person { + id: $id, + createdAt: $createdAt, + updatedAt: $updatedAt, + ownerId: $ownerId, + name: $name, + faceAssetId: ${faceAssetId ?? ""}, + isFavorite: $isFavorite, + isHidden: $isHidden, + color: ${color ?? ""}, + birthDate: ${birthDate ?? ""} +}'''; + } + + @override + bool operator ==(covariant Person other) { + if (identical(this, other)) return true; + + return other.id == id && + other.createdAt == createdAt && + other.updatedAt == updatedAt && + other.ownerId == ownerId && + other.name == name && + other.faceAssetId == faceAssetId && + other.isFavorite == isFavorite && + other.isHidden == isHidden && + other.color == color && + other.birthDate == birthDate; + } + + @override + int get hashCode { + return id.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + ownerId.hashCode ^ + name.hashCode ^ + faceAssetId.hashCode ^ + isFavorite.hashCode ^ + isHidden.hashCode ^ + color.hashCode ^ + birthDate.hashCode; + } +} diff --git a/mobile/lib/domain/models/search_result.model.dart b/mobile/lib/domain/models/search_result.model.dart new file mode 100644 index 0000000000..e8c9429432 --- /dev/null +++ b/mobile/lib/domain/models/search_result.model.dart @@ -0,0 +1,38 @@ +import 'package:collection/collection.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; + +class SearchResult { + final List assets; + final int? nextPage; + + const SearchResult({ + required this.assets, + this.nextPage, + }); + + int get totalAssets => assets.length; + + 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/domain/models/setting.model.dart b/mobile/lib/domain/models/setting.model.dart index d975cbb4fe..99246c31a1 100644 --- a/mobile/lib/domain/models/setting.model.dart +++ b/mobile/lib/domain/models/setting.model.dart @@ -3,7 +3,12 @@ import 'package:immich_mobile/domain/models/store.model.dart'; enum Setting { tilesPerRow(StoreKey.tilesPerRow, 4), groupAssetsBy(StoreKey.groupAssetsBy, 0), - showStorageIndicator(StoreKey.storageIndicator, true); + showStorageIndicator(StoreKey.storageIndicator, true), + loadOriginal(StoreKey.loadOriginal, false), + loadOriginalVideo(StoreKey.loadOriginalVideo, false), + preferRemoteImage(StoreKey.preferRemoteImage, false), + advancedTroubleshooting(StoreKey.advancedTroubleshooting, false), + ; const Setting(this.storeKey, this.defaultValue); diff --git a/mobile/lib/domain/models/stack.model.dart b/mobile/lib/domain/models/stack.model.dart new file mode 100644 index 0000000000..0db65d105b --- /dev/null +++ b/mobile/lib/domain/models/stack.model.dart @@ -0,0 +1,81 @@ +// Model for a stack stored in the server +class Stack { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + + const Stack({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + + Stack copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId, + }) { + return Stack( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + String toString() { + return '''Stack { + id: $id, + createdAt: $createdAt, + updatedAt: $updatedAt, + ownerId: $ownerId, + primaryAssetId: $primaryAssetId +}'''; + } + + @override + bool operator ==(covariant Stack other) { + if (identical(this, other)) return true; + + return other.id == id && + other.createdAt == createdAt && + other.updatedAt == updatedAt && + other.ownerId == ownerId && + other.primaryAssetId == primaryAssetId; + } + + @override + int get hashCode { + return id.hashCode ^ createdAt.hashCode ^ updatedAt.hashCode ^ ownerId.hashCode ^ primaryAssetId.hashCode; + } +} + +class StackResponse { + final String id; + final String primaryAssetId; + final List assetIds; + + const StackResponse({ + required this.id, + required this.primaryAssetId, + required this.assetIds, + }); + + @override + bool operator ==(covariant StackResponse other) { + if (identical(this, other)) return true; + + return other.id == id && other.primaryAssetId == primaryAssetId && other.assetIds == assetIds; + } + + @override + int get hashCode => id.hashCode ^ primaryAssetId.hashCode ^ assetIds.hashCode; +} diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index a96e8d3bce..305b3f3387 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -68,7 +68,10 @@ enum StoreKey { manageLocalMediaAndroid._(137), // Experimental stuff - photoManagerCustomFilter._(1000); + photoManagerCustomFilter._(1000), + betaPromptShown._(1001), + betaTimeline._(1002), + enableBackup._(1003); const StoreKey._(this.id); final int id; diff --git a/mobile/lib/domain/models/timeline.model.dart b/mobile/lib/domain/models/timeline.model.dart index 4a49708b74..98a37d619c 100644 --- a/mobile/lib/domain/models/timeline.model.dart +++ b/mobile/lib/domain/models/timeline.model.dart @@ -1,6 +1,9 @@ +import 'package:immich_mobile/domain/utils/event_stream.dart'; + enum GroupAssetsBy { day, month, + auto, none; } @@ -38,3 +41,7 @@ class TimeBucket extends Bucket { @override int get hashCode => super.hashCode ^ date.hashCode; } + +class TimelineReloadEvent extends Event { + const TimelineReloadEvent(); +} diff --git a/mobile/lib/domain/models/user.model.dart b/mobile/lib/domain/models/user.model.dart index abf2e5620b..9af8abadc2 100644 --- a/mobile/lib/domain/models/user.model.dart +++ b/mobile/lib/domain/models/user.model.dart @@ -1,3 +1,6 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + import 'package:immich_mobile/domain/models/user_metadata.model.dart'; // TODO: Rename to User once Isar is removed @@ -123,3 +126,81 @@ quotaSizeInBytes: $quotaSizeInBytes, quotaUsageInBytes.hashCode ^ quotaSizeInBytes.hashCode; } + +class PartnerUserDto { + final String id; + final String email; + final String name; + final bool inTimeline; + + final String? profileImagePath; + + const PartnerUserDto({ + required this.id, + required this.email, + required this.name, + required this.inTimeline, + this.profileImagePath, + }); + + PartnerUserDto copyWith({ + String? id, + String? email, + String? name, + bool? inTimeline, + String? profileImagePath, + }) { + return PartnerUserDto( + id: id ?? this.id, + email: email ?? this.email, + name: name ?? this.name, + inTimeline: inTimeline ?? this.inTimeline, + profileImagePath: profileImagePath ?? this.profileImagePath, + ); + } + + Map toMap() { + return { + 'id': id, + 'email': email, + 'name': name, + 'inTimeline': inTimeline, + 'profileImagePath': profileImagePath, + }; + } + + factory PartnerUserDto.fromMap(Map map) { + return PartnerUserDto( + id: map['id'] as String, + email: map['email'] as String, + name: map['name'] as String, + inTimeline: map['inTimeline'] as bool, + profileImagePath: map['profileImagePath'] != null ? map['profileImagePath'] as String : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory PartnerUserDto.fromJson(String source) => PartnerUserDto.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'PartnerUserDto(id: $id, email: $email, name: $name, inTimeline: $inTimeline, profileImagePath: $profileImagePath)'; + } + + @override + bool operator ==(covariant PartnerUserDto other) { + if (identical(this, other)) return true; + + return other.id == id && + other.email == email && + other.name == name && + other.inTimeline == inTimeline && + other.profileImagePath == profileImagePath; + } + + @override + int get hashCode { + return id.hashCode ^ email.hashCode ^ name.hashCode ^ inTimeline.hashCode ^ profileImagePath.hashCode; + } +} diff --git a/mobile/lib/domain/models/user_metadata.model.dart b/mobile/lib/domain/models/user_metadata.model.dart index 1586384422..8b7ca1ffa9 100644 --- a/mobile/lib/domain/models/user_metadata.model.dart +++ b/mobile/lib/domain/models/user_metadata.model.dart @@ -1,5 +1,12 @@ import 'dart:ui'; +enum UserMetadataKey { + // do not change this order! + onboarding, + preferences, + license, +} + enum AvatarColor { // do not change this order or reuse indices for other purposes, adding is OK primary("primary"), @@ -17,8 +24,7 @@ enum AvatarColor { const AvatarColor(this.value); Color toColor({bool isDarkTheme = false}) => switch (this) { - AvatarColor.primary => - isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF), + AvatarColor.primary => isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF), AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182), AvatarColor.red => const Color.fromARGB(255, 239, 68, 68), AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8), @@ -31,7 +37,45 @@ enum AvatarColor { }; } -class UserPreferences { +class Onboarding { + final bool isOnboarded; + + const Onboarding({required this.isOnboarded}); + + Onboarding copyWith({bool? isOnboarded}) { + return Onboarding(isOnboarded: isOnboarded ?? this.isOnboarded); + } + + Map toMap() { + final onboarding = {}; + onboarding["isOnboarded"] = isOnboarded; + return onboarding; + } + + factory Onboarding.fromMap(Map map) { + return Onboarding(isOnboarded: map["isOnboarded"] as bool); + } + + @override + String toString() { + return '''Onboarding { +isOnboarded: $isOnboarded, +}'''; + } + + @override + bool operator ==(covariant Onboarding other) { + if (identical(this, other)) return true; + + return isOnboarded == other.isOnboarded; + } + + @override + int get hashCode => isOnboarded.hashCode; +} + +// TODO: wait to be overwritten +class Preferences { final bool foldersEnabled; final bool memoriesEnabled; final bool peopleEnabled; @@ -41,7 +85,7 @@ class UserPreferences { final AvatarColor userAvatarColor; final bool showSupportBadge; - const UserPreferences({ + const Preferences({ this.foldersEnabled = false, this.memoriesEnabled = true, this.peopleEnabled = true, @@ -52,7 +96,7 @@ class UserPreferences { this.showSupportBadge = true, }); - UserPreferences copyWith({ + Preferences copyWith({ bool? foldersEnabled, bool? memoriesEnabled, bool? peopleEnabled, @@ -62,7 +106,7 @@ class UserPreferences { AvatarColor? userAvatarColor, bool? showSupportBadge, }) { - return UserPreferences( + return Preferences( foldersEnabled: foldersEnabled ?? this.foldersEnabled, memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled, peopleEnabled: peopleEnabled ?? this.peopleEnabled, @@ -87,8 +131,8 @@ class UserPreferences { return preferences; } - factory UserPreferences.fromMap(Map map) { - return UserPreferences( + factory Preferences.fromMap(Map map) { + return Preferences( foldersEnabled: map["folders-Enabled"] as bool? ?? false, memoriesEnabled: map["memories-Enabled"] as bool? ?? true, peopleEnabled: map["people-Enabled"] as bool? ?? true, @@ -102,4 +146,166 @@ class UserPreferences { showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true, ); } + + @override + String toString() { + return '''Preferences: { +foldersEnabled: $foldersEnabled, +memoriesEnabled: $memoriesEnabled, +peopleEnabled: $peopleEnabled, +ratingsEnabled: $ratingsEnabled, +sharedLinksEnabled: $sharedLinksEnabled, +tagsEnabled: $tagsEnabled, +userAvatarColor: $userAvatarColor, +showSupportBadge: $showSupportBadge, +}'''; + } + + @override + bool operator ==(covariant Preferences other) { + if (identical(this, other)) return true; + + return other.foldersEnabled == foldersEnabled && + other.memoriesEnabled == memoriesEnabled && + other.peopleEnabled == peopleEnabled && + other.ratingsEnabled == ratingsEnabled && + other.sharedLinksEnabled == sharedLinksEnabled && + other.tagsEnabled == tagsEnabled && + other.userAvatarColor == userAvatarColor && + other.showSupportBadge == showSupportBadge; + } + + @override + int get hashCode { + return foldersEnabled.hashCode ^ + memoriesEnabled.hashCode ^ + peopleEnabled.hashCode ^ + ratingsEnabled.hashCode ^ + sharedLinksEnabled.hashCode ^ + tagsEnabled.hashCode ^ + userAvatarColor.hashCode ^ + showSupportBadge.hashCode; + } +} + +class License { + final DateTime activatedAt; + final String activationKey; + final String licenseKey; + + const License({ + required this.activatedAt, + required this.activationKey, + required this.licenseKey, + }); + + License copyWith({ + DateTime? activatedAt, + String? activationKey, + String? licenseKey, + }) { + return License( + activatedAt: activatedAt ?? this.activatedAt, + activationKey: activationKey ?? this.activationKey, + licenseKey: licenseKey ?? this.licenseKey, + ); + } + + Map toMap() { + final license = {}; + license["activatedAt"] = activatedAt; + license["activationKey"] = activationKey; + license["licenseKey"] = licenseKey; + return license; + } + + factory License.fromMap(Map map) { + return License( + activatedAt: map["activatedAt"] as DateTime, + activationKey: map["activationKey"] as String, + licenseKey: map["licenseKey"] as String, + ); + } + + @override + String toString() { + return '''License { +activatedAt: $activatedAt, +activationKey: $activationKey, +licenseKey: $licenseKey, +}'''; + } + + @override + bool operator ==(covariant License other) { + if (identical(this, other)) return true; + + return activatedAt == other.activatedAt && activationKey == other.activationKey && licenseKey == other.licenseKey; + } + + @override + int get hashCode => activatedAt.hashCode ^ activationKey.hashCode ^ licenseKey.hashCode; +} + +// Model for a user metadata stored in the server +class UserMetadata { + final String userId; + final UserMetadataKey key; + final Onboarding? onboarding; + final Preferences? preferences; + final License? license; + + const UserMetadata({ + required this.userId, + required this.key, + this.onboarding, + this.preferences, + this.license, + }) : assert( + onboarding != null || preferences != null || license != null, + 'One of onboarding, preferences and license must be provided', + ); + + UserMetadata copyWith({ + String? userId, + UserMetadataKey? key, + Onboarding? onboarding, + Preferences? preferences, + License? license, + }) { + return UserMetadata( + userId: userId ?? this.userId, + key: key ?? this.key, + onboarding: onboarding ?? this.onboarding, + preferences: preferences ?? this.preferences, + license: license ?? this.license, + ); + } + + @override + String toString() { + return '''UserMetadata: { +userId: $userId, +key: $key, +onboarding: ${onboarding ?? ""}, +preferences: ${preferences ?? ""}, +license: ${license ?? ""}, +}'''; + } + + @override + bool operator ==(covariant UserMetadata other) { + if (identical(this, other)) return true; + + return other.userId == userId && + other.key == key && + other.onboarding == onboarding && + other.preferences == preferences && + other.license == license; + } + + @override + int get hashCode { + return userId.hashCode ^ key.hashCode ^ onboarding.hashCode ^ preferences.hashCode ^ license.hashCode; + } } diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart new file mode 100644 index 0000000000..5006e2d45c --- /dev/null +++ b/mobile/lib/domain/services/asset.service.dart @@ -0,0 +1,87 @@ +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; +import 'package:platform/platform.dart'; + +class AssetService { + final RemoteAssetRepository _remoteAssetRepository; + final DriftLocalAssetRepository _localAssetRepository; + final Platform _platform; + + const AssetService({ + required RemoteAssetRepository remoteAssetRepository, + required DriftLocalAssetRepository localAssetRepository, + }) : _remoteAssetRepository = remoteAssetRepository, + _localAssetRepository = localAssetRepository, + _platform = const LocalPlatform(); + + Stream watchAsset(BaseAsset asset) { + final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id; + return asset is LocalAsset ? _localAssetRepository.watchAsset(id) : _remoteAssetRepository.watchAsset(id); + } + + Future getRemoteAsset(String id) { + return _remoteAssetRepository.get(id); + } + + Future> getStack(RemoteAsset asset) async { + if (asset.stackId == null) { + return []; + } + + return _remoteAssetRepository.getStackChildren(asset).then((assets) { + // Include the primary asset in the stack as the first item + return [asset, ...assets]; + }); + } + + Future getExif(BaseAsset asset) async { + if (!asset.hasRemote) { + return null; + } + + final id = asset is LocalAsset ? asset.remoteId! : (asset as RemoteAsset).id; + return _remoteAssetRepository.getExif(id); + } + + Future getAspectRatio(BaseAsset asset) async { + bool isFlipped; + double? width; + double? height; + + if (asset.hasRemote) { + final exif = await getExif(asset); + isFlipped = ExifDtoConverter.isOrientationFlipped(exif?.orientation); + width = exif?.width ?? asset.width?.toDouble(); + height = exif?.height ?? asset.height?.toDouble(); + } else if (asset is LocalAsset) { + isFlipped = _platform.isAndroid && (asset.orientation == 90 || asset.orientation == 270); + width = asset.width?.toDouble(); + height = asset.height?.toDouble(); + } else { + isFlipped = false; + } + + final orientedWidth = isFlipped ? height : width; + final orientedHeight = isFlipped ? width : height; + if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) { + return orientedWidth / orientedHeight; + } + + return 1.0; + } + + Future> getPlaces() { + return _remoteAssetRepository.getPlaces(); + } + + Future<(int local, int remote)> getAssetCounts() async { + return (await _localAssetRepository.getCount(), await _remoteAssetRepository.getCount()); + } + + Future getLocalHashedCount() { + return _localAssetRepository.getHashedCount(); + } +} diff --git a/mobile/lib/domain/services/hash.service.dart b/mobile/lib/domain/services/hash.service.dart index d8ad3dc2dc..3bbb75b003 100644 --- a/mobile/lib/domain/services/hash.service.dart +++ b/mobile/lib/domain/services/hash.service.dart @@ -1,10 +1,10 @@ import 'dart:convert'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; -import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart'; -import 'package:immich_mobile/domain/interfaces/storage.interface.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; import 'package:logging/logging.dart'; @@ -12,16 +12,16 @@ import 'package:logging/logging.dart'; class HashService { final int batchSizeLimit; final int batchFileLimit; - final ILocalAlbumRepository _localAlbumRepository; - final ILocalAssetRepository _localAssetRepository; - final IStorageRepository _storageRepository; + final DriftLocalAlbumRepository _localAlbumRepository; + final DriftLocalAssetRepository _localAssetRepository; + final StorageRepository _storageRepository; final NativeSyncApi _nativeSyncApi; final _log = Logger('HashService'); HashService({ - required ILocalAlbumRepository localAlbumRepository, - required ILocalAssetRepository localAssetRepository, - required IStorageRepository storageRepository, + required DriftLocalAlbumRepository localAlbumRepository, + required DriftLocalAssetRepository localAssetRepository, + required StorageRepository storageRepository, required NativeSyncApi nativeSyncApi, this.batchSizeLimit = kBatchHashSizeLimit, this.batchFileLimit = kBatchHashFileLimit, @@ -41,8 +41,7 @@ class HashService { ); for (final album in localAlbums) { - final assetsToHash = - await _localAlbumRepository.getAssetsToHash(album.id); + final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id); if (assetsToHash.isNotEmpty) { await _hashAssets(assetsToHash); } @@ -61,7 +60,7 @@ class HashService { final toHash = <_AssetToPath>[]; for (final asset in assetsToHash) { - final file = await _storageRepository.getFileForAsset(asset); + final file = await _storageRepository.getFileForAsset(asset.id); if (file == null) { continue; } @@ -88,8 +87,7 @@ class HashService { _log.fine("Hashing ${toHash.length} files"); final hashed = []; - final hashes = - await _nativeSyncApi.hashPaths(toHash.map((e) => e.path).toList()); + final hashes = await _nativeSyncApi.hashPaths(toHash.map((e) => e.path).toList()); assert( hashes.length == toHash.length, "Hashes length does not match toHash length: ${hashes.length} != ${toHash.length}", diff --git a/mobile/lib/domain/services/local_album.service.dart b/mobile/lib/domain/services/local_album.service.dart new file mode 100644 index 0000000000..6c1479fdc9 --- /dev/null +++ b/mobile/lib/domain/services/local_album.service.dart @@ -0,0 +1,25 @@ +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; + +class LocalAlbumService { + final DriftLocalAlbumRepository _repository; + + const LocalAlbumService(this._repository); + + Future> getAll({Set sortBy = const {}}) { + return _repository.getAll(sortBy: sortBy); + } + + Future getThumbnail(String albumId) { + return _repository.getThumbnail(albumId); + } + + Future update(LocalAlbum album) { + return _repository.upsert(album); + } + + Future getCount() { + return _repository.getCount(); + } +} diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index ff77ebd83e..4204761054 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -2,11 +2,9 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; -import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/local_album.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; import 'package:immich_mobile/utils/diff.dart'; @@ -14,25 +12,19 @@ import 'package:logging/logging.dart'; import 'package:platform/platform.dart'; class LocalSyncService { - final ILocalAlbumRepository _localAlbumRepository; + final DriftLocalAlbumRepository _localAlbumRepository; final NativeSyncApi _nativeSyncApi; final Platform _platform; - final StoreService _storeService; final Logger _log = Logger("DeviceSyncService"); LocalSyncService({ - required ILocalAlbumRepository localAlbumRepository, + required DriftLocalAlbumRepository localAlbumRepository, required NativeSyncApi nativeSyncApi, - required StoreService storeService, Platform? platform, }) : _localAlbumRepository = localAlbumRepository, _nativeSyncApi = nativeSyncApi, - _storeService = storeService, _platform = platform ?? const LocalPlatform(); - bool get _ignoreIcloudAssets => - _storeService.get(StoreKey.ignoreIcloudAssets, false) == true; - Future sync({bool full = false}) async { final Stopwatch stopwatch = Stopwatch()..start(); try { @@ -74,8 +66,7 @@ class LocalSyncService { // On iOS, we need to full sync albums that are marked as cloud as the delta sync // does not include changes for cloud albums. If ignoreIcloudAssets is enabled, // remove the albums from the local database from the previous sync - final cloudAlbums = - deviceAlbums.where((a) => a.isCloud).toLocalAlbums(); + final cloudAlbums = deviceAlbums.where((a) => a.isCloud).toLocalAlbums(); for (final album in cloudAlbums) { final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id); if (dbAlbum == null) { @@ -84,11 +75,7 @@ class LocalSyncService { ); continue; } - if (_ignoreIcloudAssets) { - await removeAlbum(dbAlbum); - } else { - await updateAlbum(dbAlbum, album); - } + await updateAlbum(dbAlbum, album); } } @@ -106,14 +93,8 @@ class LocalSyncService { try { final Stopwatch stopwatch = Stopwatch()..start(); - List deviceAlbums = - List.of(await _nativeSyncApi.getAlbums()); - if (_platform.isIOS && _ignoreIcloudAssets) { - deviceAlbums.removeWhere((album) => album.isCloud); - } - - final dbAlbums = - await _localAlbumRepository.getAll(sortBy: {SortLocalAlbumsBy.id}); + final deviceAlbums = await _nativeSyncApi.getAlbums(); + final dbAlbums = await _localAlbumRepository.getAll(sortBy: {SortLocalAlbumsBy.id}); await diffSortedLists( dbAlbums, @@ -137,9 +118,7 @@ class LocalSyncService { try { _log.fine("Adding device album ${album.name}"); - final assets = album.assetCount > 0 - ? await _nativeSyncApi.getAssetsForAlbum(album.id) - : []; + final assets = album.assetCount > 0 ? await _nativeSyncApi.getAssetsForAlbum(album.id) : []; await _localAlbumRepository.upsert( album, @@ -205,10 +184,8 @@ class LocalSyncService { return false; } - final updatedTime = - (dbAlbum.updatedAt.millisecondsSinceEpoch ~/ 1000) + 1; - final newAssetsCount = - await _nativeSyncApi.getAssetsCountSince(deviceAlbum.id, updatedTime); + final updatedTime = (dbAlbum.updatedAt.millisecondsSinceEpoch ~/ 1000) + 1; + final newAssetsCount = await _nativeSyncApi.getAssetsCountSince(deviceAlbum.id, updatedTime); // Early return if no new assets were found if (newAssetsCount == 0) { @@ -247,13 +224,9 @@ class LocalSyncService { Future fullDiff(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async { try { final assetsInDevice = deviceAlbum.assetCount > 0 - ? await _nativeSyncApi - .getAssetsForAlbum(deviceAlbum.id) - .then((a) => a.toLocalAssets()) - : []; - final assetsInDb = dbAlbum.assetCount > 0 - ? await _localAlbumRepository.getAssets(dbAlbum.id) + ? await _nativeSyncApi.getAssetsForAlbum(deviceAlbum.id).then((a) => a.toLocalAssets()) : []; + final assetsInDb = dbAlbum.assetCount > 0 ? await _localAlbumRepository.getAssets(dbAlbum.id) : []; if (deviceAlbum.assetCount == 0) { _log.fine( @@ -338,9 +311,7 @@ class LocalSyncService { } bool _albumsEqual(LocalAlbum a, LocalAlbum b) { - return a.name == b.name && - a.assetCount == b.assetCount && - a.updatedAt.isAtSameMomentAs(b.updatedAt); + return a.name == b.name && a.assetCount == b.assetCount && a.updatedAt.isAtSameMomentAs(b.updatedAt); } } @@ -350,9 +321,7 @@ extension on Iterable { (e) => LocalAlbum( id: e.id, name: e.name, - updatedAt: e.updatedAt == null - ? DateTime.now() - : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), + updatedAt: e.updatedAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), assetCount: e.assetCount, ), ).toList(); @@ -367,15 +336,12 @@ extension on Iterable { name: e.name, checksum: null, type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other, - createdAt: e.createdAt == null - ? DateTime.now() - : DateTime.fromMillisecondsSinceEpoch(e.createdAt! * 1000), - updatedAt: e.updatedAt == null - ? DateTime.now() - : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), + createdAt: e.createdAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.createdAt! * 1000), + updatedAt: e.updatedAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), width: e.width, height: e.height, durationInSeconds: e.durationInSeconds, + orientation: e.orientation, ), ).toList(); } diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index a25147d185..72fb4d9bf7 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/interfaces/log.interface.dart'; -import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:logging/logging.dart'; /// Service responsible for handling application logging. @@ -14,8 +14,8 @@ import 'package:logging/logging.dart'; /// writes them to a persistent [ILogRepository], and manages log levels /// via [IStoreRepository] class LogService { - final ILogRepository _logRepository; - final IStoreRepository _storeRepository; + final IsarLogRepository _logRepository; + final IsarStoreRepository _storeRepository; final List _msgBuffer = []; @@ -37,8 +37,8 @@ class LogService { } static Future init({ - required ILogRepository logRepository, - required IStoreRepository storeRepository, + required IsarLogRepository logRepository, + required IsarStoreRepository storeRepository, bool shouldBuffer = true, }) async { _instance ??= await create( @@ -50,14 +50,13 @@ class LogService { } static Future create({ - required ILogRepository logRepository, - required IStoreRepository storeRepository, + required IsarLogRepository logRepository, + required IsarStoreRepository storeRepository, bool shouldBuffer = true, }) async { final instance = LogService._(logRepository, storeRepository, shouldBuffer); await logRepository.truncate(limit: kLogTruncateLimit); - final level = await instance._storeRepository.tryGet(StoreKey.logLevel) ?? - LogLevel.info.index; + final level = await instance._storeRepository.tryGet(StoreKey.logLevel) ?? LogLevel.info.index; Logger.root.level = Level.LEVELS.elementAtOrNull(level) ?? Level.INFO; return instance; } @@ -146,9 +145,7 @@ class LoggerUnInitializedException implements Exception { /// Log levels according to dart logging [Level] extension LevelDomainToInfraExtension on Level { - LogLevel toLogLevel() => - LogLevel.values.elementAtOrNull(Level.LEVELS.indexOf(this)) ?? - LogLevel.info; + LogLevel toLogLevel() => LogLevel.values.elementAtOrNull(Level.LEVELS.indexOf(this)) ?? LogLevel.info; } extension on LogLevel { diff --git a/mobile/lib/domain/services/memory.service.dart b/mobile/lib/domain/services/memory.service.dart new file mode 100644 index 0000000000..ead613370f --- /dev/null +++ b/mobile/lib/domain/services/memory.service.dart @@ -0,0 +1,23 @@ +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart'; +import 'package:logging/logging.dart'; + +class DriftMemoryService { + final log = Logger("DriftMemoryService"); + + final DriftMemoryRepository _repository; + + DriftMemoryService(this._repository); + + Future> getMemoryLane(String ownerId) { + return _repository.getAll(ownerId); + } + + Future get(String memoryId) { + return _repository.get(memoryId); + } + + Future getCount() { + return _repository.getCount(); + } +} diff --git a/mobile/lib/domain/services/partner.service.dart b/mobile/lib/domain/services/partner.service.dart new file mode 100644 index 0000000000..11299b9d6d --- /dev/null +++ b/mobile/lib/domain/services/partner.service.dart @@ -0,0 +1,59 @@ +import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; +import 'package:immich_mobile/repositories/partner_api.repository.dart'; + +class DriftPartnerService { + final DriftPartnerRepository _driftPartnerRepository; + final PartnerApiRepository _partnerApiRepository; + + const DriftPartnerService( + this._driftPartnerRepository, + this._partnerApiRepository, + ); + + Future> getSharedWith(String userId) { + return _driftPartnerRepository.getSharedWith(userId); + } + + Future> getSharedBy(String userId) { + return _driftPartnerRepository.getSharedBy(userId); + } + + Future> getAvailablePartners( + String currentUserId, + ) async { + final otherUsers = await _driftPartnerRepository.getAvailablePartners(currentUserId); + final currentPartners = await _driftPartnerRepository.getSharedBy(currentUserId); + final available = otherUsers.where((user) { + return !currentPartners.any((partner) => partner.id == user.id); + }).toList(); + + return available; + } + + Future toggleShowInTimeline(String partnerId, String userId) async { + final partner = await _driftPartnerRepository.getPartner(partnerId, userId); + if (partner == null) { + debugPrint("Partner not found: $partnerId for user: $userId"); + return; + } + + await _partnerApiRepository.update( + partnerId, + inTimeline: !partner.inTimeline, + ); + + await _driftPartnerRepository.toggleShowInTimeline(partner, userId); + } + + Future addPartner(String partnerId, String userId) async { + await _partnerApiRepository.create(partnerId); + await _driftPartnerRepository.create(partnerId, userId); + } + + Future removePartner(String partnerId, String userId) async { + await _partnerApiRepository.delete(partnerId); + await _driftPartnerRepository.delete(partnerId, userId); + } +} diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart new file mode 100644 index 0000000000..6c3b2e32af --- /dev/null +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -0,0 +1,155 @@ +import 'dart:async'; + +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; +import 'package:immich_mobile/models/albums/album_search.model.dart'; +import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; +import 'package:immich_mobile/utils/remote_album.utils.dart'; + +class RemoteAlbumService { + final DriftRemoteAlbumRepository _repository; + final DriftAlbumApiRepository _albumApiRepository; + + const RemoteAlbumService(this._repository, this._albumApiRepository); + + Stream watchAlbum(String albumId) { + return _repository.watchAlbum(albumId); + } + + Future> getAll() { + return _repository.getAll(); + } + + Future get(String albumId) { + return _repository.get(albumId); + } + + List sortAlbums( + List albums, + RemoteAlbumSortMode sortMode, { + bool isReverse = false, + }) { + return sortMode.sortFn(albums, isReverse); + } + + List searchAlbums( + List albums, + String query, + String? userId, [ + QuickFilterMode filterMode = QuickFilterMode.all, + ]) { + final lowerQuery = query.toLowerCase(); + List filtered = albums; + + // Apply text search filter + if (query.isNotEmpty) { + filtered = filtered + .where( + (album) => + album.name.toLowerCase().contains(lowerQuery) || album.description.toLowerCase().contains(lowerQuery), + ) + .toList(); + } + + if (userId != null) { + switch (filterMode) { + case QuickFilterMode.myAlbums: + filtered = filtered.where((album) => album.ownerId == userId).toList(); + break; + case QuickFilterMode.sharedWithMe: + filtered = filtered.where((album) => album.ownerId != userId).toList(); + break; + case QuickFilterMode.all: + break; + } + } + + return filtered; + } + + Future createAlbum({ + required String title, + required List assetIds, + String? description, + }) async { + final album = await _albumApiRepository.createDriftAlbum( + title, + description: description, + assetIds: assetIds, + ); + + await _repository.create(album, assetIds); + + return album; + } + + Future updateAlbum( + String albumId, { + String? name, + String? description, + String? thumbnailAssetId, + bool? isActivityEnabled, + AlbumAssetOrder? order, + }) async { + final updatedAlbum = await _albumApiRepository.updateAlbum( + albumId, + name: name, + description: description, + thumbnailAssetId: thumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: order, + ); + + // Update the local database + await _repository.update(updatedAlbum); + + return updatedAlbum; + } + + FutureOr<(DateTime, DateTime)> getDateRange(String albumId) { + return _repository.getDateRange(albumId); + } + + Future> getSharedUsers(String albumId) { + return _repository.getSharedUsers(albumId); + } + + Future> getAssets(String albumId) { + return _repository.getAssets(albumId); + } + + Future addAssets({ + required String albumId, + required List assetIds, + }) async { + final album = await _albumApiRepository.addAssets( + albumId, + assetIds, + ); + + await _repository.addAssets(albumId, album.added); + + return album.added.length; + } + + Future deleteAlbum(String albumId) async { + await _albumApiRepository.deleteAlbum(albumId); + + await _repository.deleteAlbum(albumId); + } + + Future addUsers({ + required String albumId, + required List userIds, + }) async { + await _albumApiRepository.addUsers(albumId, userIds); + + return _repository.addUsers(albumId, userIds); + } + + Future getCount() { + return _repository.getCount(); + } +} diff --git a/mobile/lib/domain/services/search.service.dart b/mobile/lib/domain/services/search.service.dart new file mode 100644 index 0000000000..052a2ca9da --- /dev/null +++ b/mobile/lib/domain/services/search.service.dart @@ -0,0 +1,92 @@ +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/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 { + final _log = Logger("SearchService"); + final SearchApiRepository _searchApiRepository; + + SearchService(this._searchApiRepository); + + Future?> getSearchSuggestions( + SearchSuggestionType type, { + String? country, + String? state, + String? make, + String? model, + }) async { + try { + return await _searchApiRepository.getSearchSuggestions( + type, + country: country, + state: state, + make: make, + model: model, + ); + } catch (e) { + _log.warning("Failed to get search suggestions", e); + } + return []; + } + + 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: response.assets.items.map((e) => e.toDto()).toList(), + nextPage: response.assets.nextPage?.toInt(), + ); + } catch (error, stackTrace) { + _log.severe("Failed to search for assets", error, stackTrace); + } + 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: exifInfo?.exifImageHeight?.toInt(), + width: exifInfo?.exifImageWidth?.toInt(), + isFavorite: isFavorite, + livePhotoVideoId: livePhotoVideoId, + thumbHash: thumbhash, + localId: null, + type: type.toAssetType(), + ); + } +} + +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/setting.service.dart b/mobile/lib/domain/services/setting.service.dart index 2d1937be5a..99e07a2872 100644 --- a/mobile/lib/domain/services/setting.service.dart +++ b/mobile/lib/domain/services/setting.service.dart @@ -1,19 +1,19 @@ import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +// Singleton instance of SettingsService, to use in places +// where reactivity is not required +// ignore: non_constant_identifier_names +final AppSetting = SettingsService(storeService: StoreService.I); + class SettingsService { final StoreService _storeService; - const SettingsService({required StoreService storeService}) - : _storeService = storeService; + const SettingsService({required StoreService storeService}) : _storeService = storeService; - T get(Setting setting) => - _storeService.get(setting.storeKey, setting.defaultValue); + T get(Setting setting) => _storeService.get(setting.storeKey, setting.defaultValue); - Future set(Setting setting, T value) => - _storeService.put(setting.storeKey, value); + Future set(Setting setting, T value) => _storeService.put(setting.storeKey, value); - Stream watch(Setting setting) => _storeService - .watch(setting.storeKey) - .map((v) => v ?? setting.defaultValue); + Stream watch(Setting setting) => _storeService.watch(setting.storeKey).map((v) => v ?? setting.defaultValue); } diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart index 7b71acd254..bd839a18ec 100644 --- a/mobile/lib/domain/services/store.service.dart +++ b/mobile/lib/domain/services/store.service.dart @@ -1,19 +1,18 @@ import 'dart:async'; -import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +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 IsarStoreRepository _storeRepository; /// In-memory cache. Keys are [StoreKey.id] final Map _cache = {}; late final StreamSubscription _storeUpdateSubscription; - StoreService._({required IStoreRepository storeRepository}) - : _storeRepository = storeRepository; + StoreService._({required IsarStoreRepository storeRepository}) : _storeRepository = storeRepository; // TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider static StoreService? _instance; @@ -26,14 +25,14 @@ class StoreService { // TODO: Replace the implementation with the one from create after removing the typedef static Future init({ - required IStoreRepository storeRepository, + required IsarStoreRepository storeRepository, }) async { _instance ??= await create(storeRepository: storeRepository); return _instance!; } static Future create({ - required IStoreRepository storeRepository, + required IsarStoreRepository storeRepository, }) async { final instance = StoreService._(storeRepository: storeRepository); await instance._populateCache(); @@ -48,8 +47,7 @@ class StoreService { } } - StreamSubscription _listenForChange() => - _storeRepository.watchAll().listen((event) { + StreamSubscription _listenForChange() => _storeRepository.watchAll().listen((event) { _cache[event.key.id] = event.value; }); @@ -93,6 +91,8 @@ class StoreService { await _storeRepository.deleteAll(); _cache.clear(); } + + bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? false; } class StoreKeyNotFoundException implements Exception { diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index f8c76b9543..a985008251 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -1,20 +1,21 @@ import 'dart:async'; -import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; class SyncStreamService { final Logger _logger = Logger('SyncStreamService'); - final ISyncApiRepository _syncApiRepository; + final SyncApiRepository _syncApiRepository; final SyncStreamRepository _syncStreamRepository; final bool Function()? _cancelChecker; SyncStreamService({ - required ISyncApiRepository syncApiRepository, + required SyncApiRepository syncApiRepository, required SyncStreamRepository syncStreamRepository, bool Function()? cancelChecker, }) : _syncApiRepository = syncApiRepository, @@ -23,7 +24,65 @@ class SyncStreamService { bool get isCancelled => _cancelChecker?.call() ?? false; - Future sync() => _syncApiRepository.streamChanges(_handleEvents); + Future sync() { + _logger.info("Remote sync request for user"); + DLog.log("Remote sync request for user"); + // Start the sync stream and handle events + return _syncApiRepository.streamChanges(_handleEvents); + } + + Future handleWsAssetUploadReadyV1Batch(List batchData) async { + if (batchData.isEmpty) return; + + _logger.info( + 'Processing batch of ${batchData.length} AssetUploadReadyV1 events', + ); + + final List assets = []; + final List exifs = []; + + try { + for (final data in batchData) { + if (data is! Map) { + continue; + } + + final payload = data; + final assetData = payload['asset']; + final exifData = payload['exif']; + + if (assetData == null || exifData == null) { + continue; + } + + final asset = SyncAssetV1.fromJson(assetData); + final exif = SyncAssetExifV1.fromJson(exifData); + + if (asset != null && exif != null) { + assets.add(asset); + exifs.add(exif); + } + } + + if (assets.isNotEmpty && exifs.isNotEmpty) { + await _syncStreamRepository.updateAssetsV1( + assets, + debugLabel: 'websocket-batch', + ); + await _syncStreamRepository.updateAssetsExifV1( + exifs, + debugLabel: 'websocket-batch', + ); + _logger.info('Successfully processed ${assets.length} assets in batch'); + } + } catch (error, stackTrace) { + _logger.severe( + "Error processing AssetUploadReadyV1 websocket batch events", + error, + stackTrace, + ); + } + } Future _handleEvents(List events, Function() abort) async { List items = []; @@ -76,11 +135,119 @@ class SyncStreamService { case SyncEntityType.assetExifV1: return _syncStreamRepository.updateAssetsExifV1(data.cast()); case SyncEntityType.partnerAssetV1: - return _syncStreamRepository.updatePartnerAssetsV1(data.cast()); + return _syncStreamRepository.updateAssetsV1( + data.cast(), + debugLabel: 'partner', + ); + case SyncEntityType.partnerAssetBackfillV1: + return _syncStreamRepository.updateAssetsV1( + data.cast(), + debugLabel: 'partner backfill', + ); case SyncEntityType.partnerAssetDeleteV1: - return _syncStreamRepository.deletePartnerAssetsV1(data.cast()); + return _syncStreamRepository.deleteAssetsV1( + data.cast(), + debugLabel: "partner", + ); case SyncEntityType.partnerAssetExifV1: - return _syncStreamRepository.updatePartnerAssetsExifV1(data.cast()); + return _syncStreamRepository.updateAssetsExifV1( + data.cast(), + debugLabel: 'partner', + ); + case SyncEntityType.partnerAssetExifBackfillV1: + return _syncStreamRepository.updateAssetsExifV1( + data.cast(), + debugLabel: 'partner backfill', + ); + case SyncEntityType.albumV1: + return _syncStreamRepository.updateAlbumsV1(data.cast()); + case SyncEntityType.albumDeleteV1: + return _syncStreamRepository.deleteAlbumsV1(data.cast()); + case SyncEntityType.albumUserV1: + return _syncStreamRepository.updateAlbumUsersV1(data.cast()); + case SyncEntityType.albumUserBackfillV1: + return _syncStreamRepository.updateAlbumUsersV1( + data.cast(), + debugLabel: 'backfill', + ); + case SyncEntityType.albumUserDeleteV1: + return _syncStreamRepository.deleteAlbumUsersV1(data.cast()); + case SyncEntityType.albumAssetV1: + return _syncStreamRepository.updateAssetsV1( + data.cast(), + debugLabel: 'album', + ); + case SyncEntityType.albumAssetBackfillV1: + return _syncStreamRepository.updateAssetsV1( + data.cast(), + debugLabel: 'album backfill', + ); + case SyncEntityType.albumAssetExifV1: + return _syncStreamRepository.updateAssetsExifV1( + data.cast(), + debugLabel: 'album', + ); + case SyncEntityType.albumAssetExifBackfillV1: + return _syncStreamRepository.updateAssetsExifV1( + data.cast(), + debugLabel: 'album backfill', + ); + case SyncEntityType.albumToAssetV1: + return _syncStreamRepository.updateAlbumToAssetsV1(data.cast()); + case SyncEntityType.albumToAssetBackfillV1: + return _syncStreamRepository.updateAlbumToAssetsV1( + data.cast(), + debugLabel: 'backfill', + ); + case SyncEntityType.albumToAssetDeleteV1: + return _syncStreamRepository.deleteAlbumToAssetsV1(data.cast()); + // No-op. SyncAckV1 entities are checkpoints in the sync stream + // to acknowledge that the client has processed all the backfill events + case SyncEntityType.syncAckV1: + return; + case SyncEntityType.memoryV1: + return _syncStreamRepository.updateMemoriesV1(data.cast()); + case SyncEntityType.memoryDeleteV1: + return _syncStreamRepository.deleteMemoriesV1(data.cast()); + case SyncEntityType.memoryToAssetV1: + return _syncStreamRepository.updateMemoryAssetsV1(data.cast()); + case SyncEntityType.memoryToAssetDeleteV1: + return _syncStreamRepository.deleteMemoryAssetsV1(data.cast()); + case SyncEntityType.stackV1: + return _syncStreamRepository.updateStacksV1(data.cast()); + case SyncEntityType.stackDeleteV1: + return _syncStreamRepository.deleteStacksV1(data.cast()); + case SyncEntityType.partnerStackV1: + return _syncStreamRepository.updateStacksV1( + data.cast(), + debugLabel: 'partner', + ); + case SyncEntityType.partnerStackBackfillV1: + return _syncStreamRepository.updateStacksV1( + data.cast(), + debugLabel: 'partner backfill', + ); + case SyncEntityType.partnerStackDeleteV1: + return _syncStreamRepository.deleteStacksV1( + data.cast(), + debugLabel: 'partner', + ); + case SyncEntityType.userMetadataV1: + return _syncStreamRepository.updateUserMetadatasV1( + data.cast(), + ); + case SyncEntityType.userMetadataDeleteV1: + return _syncStreamRepository.deleteUserMetadatasV1( + data.cast(), + ); + case SyncEntityType.personV1: + return _syncStreamRepository.updatePeopleV1(data.cast()); + case SyncEntityType.personDeleteV1: + return _syncStreamRepository.deletePeopleV1(data.cast()); + case SyncEntityType.assetFaceV1: + return _syncStreamRepository.updateAssetFacesV1(data.cast()); + case SyncEntityType.assetFaceDeleteV1: + return _syncStreamRepository.deleteAssetFacesV1(data.cast()); default: _logger.warning("Unknown sync data type: $type"); } diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index d1211f46e2..7e982558b7 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -3,11 +3,12 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/interfaces/timeline.interface.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/setting.service.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; typedef TimelineAssetSource = Future> Function( @@ -17,62 +18,110 @@ typedef TimelineAssetSource = Future> Function( typedef TimelineBucketSource = Stream> Function(); +typedef TimelineQuery = ({ + TimelineAssetSource assetSource, + TimelineBucketSource bucketSource, +}); + class TimelineFactory { - final ITimelineRepository _timelineRepository; + final DriftTimelineRepository _timelineRepository; final SettingsService _settingsService; const TimelineFactory({ - required ITimelineRepository timelineRepository, + required DriftTimelineRepository timelineRepository, required SettingsService settingsService, }) : _timelineRepository = timelineRepository, _settingsService = settingsService; - GroupAssetsBy get groupBy => - GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)]; + GroupAssetsBy get groupBy { + final group = GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)]; + // We do not support auto grouping in the new timeline yet, fallback to day grouping + return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group; + } - TimelineService main(List timelineUsers) => TimelineService( - assetSource: (offset, count) => _timelineRepository - .getMainBucketAssets(timelineUsers, offset: offset, count: count), - bucketSource: () => _timelineRepository.watchMainBucket( - timelineUsers, - groupBy: groupBy, - ), - ); + TimelineService main(List timelineUsers) => TimelineService(_timelineRepository.main(timelineUsers, groupBy)); - TimelineService localAlbum({required String albumId}) => TimelineService( - assetSource: (offset, count) => _timelineRepository - .getLocalBucketAssets(albumId, offset: offset, count: count), - bucketSource: () => - _timelineRepository.watchLocalBucket(albumId, groupBy: groupBy), - ); + TimelineService localAlbum({required String albumId}) => + TimelineService(_timelineRepository.localAlbum(albumId, groupBy)); + + TimelineService remoteAlbum({required String albumId}) => + TimelineService(_timelineRepository.remoteAlbum(albumId, groupBy)); + + TimelineService remoteAssets(String userId) => TimelineService(_timelineRepository.remote(userId, groupBy)); + + TimelineService favorite(String userId) => TimelineService(_timelineRepository.favorite(userId, groupBy)); + + TimelineService trash(String userId) => TimelineService(_timelineRepository.trash(userId, groupBy)); + + TimelineService archive(String userId) => TimelineService(_timelineRepository.archived(userId, groupBy)); + + TimelineService lockedFolder(String userId) => TimelineService(_timelineRepository.locked(userId, groupBy)); + + TimelineService video(String userId) => TimelineService(_timelineRepository.video(userId, groupBy)); + + TimelineService place(String place) => TimelineService(_timelineRepository.place(place, groupBy)); + + TimelineService fromAssets(List assets) => TimelineService(_timelineRepository.fromAssets(assets)); } class TimelineService { final TimelineAssetSource _assetSource; final TimelineBucketSource _bucketSource; - - TimelineService({ - required TimelineAssetSource assetSource, - required TimelineBucketSource bucketSource, - }) : _assetSource = assetSource, - _bucketSource = bucketSource { - _bucketSubscription = - _bucketSource().listen((_) => unawaited(_reloadBucket())); - } - final AsyncMutex _mutex = AsyncMutex(); int _bufferOffset = 0; List _buffer = []; StreamSubscription? _bucketSubscription; + int _totalAssets = 0; + int get totalAssets => _totalAssets; + + TimelineService(TimelineQuery query) + : this._( + assetSource: query.assetSource, + bucketSource: query.bucketSource, + ); + + TimelineService._({ + required TimelineAssetSource assetSource, + required TimelineBucketSource bucketSource, + }) : _assetSource = assetSource, + _bucketSource = bucketSource { + _bucketSubscription = _bucketSource().listen((buckets) { + _mutex.run(() async { + final totalAssets = buckets.fold(0, (acc, bucket) => acc + bucket.assetCount); + + if (totalAssets == 0) { + _bufferOffset = 0; + _buffer.clear(); + } else { + final int offset; + final int count; + // When the buffer is empty or the old bufferOffset is greater than the new total assets, + // we need to reset the buffer and load the first batch of assets. + if (_bufferOffset >= totalAssets || _buffer.isEmpty) { + offset = 0; + count = kTimelineAssetLoadBatchSize; + } else { + offset = _bufferOffset; + count = math.min( + _buffer.length, + totalAssets - _bufferOffset, + ); + } + _buffer = await _assetSource(offset, count); + _bufferOffset = offset; + } + + // change the state's total assets count only after the buffer is reloaded + _totalAssets = totalAssets; + EventStream.shared.emit(const TimelineReloadEvent()); + }); + }); + } + Stream> Function() get watchBuckets => _bucketSource; - Future _reloadBucket() => _mutex.run(() async { - _buffer = await _assetSource(_bufferOffset, _buffer.length); - }); - - Future> loadAssets(int index, int count) => - _mutex.run(() => _loadAssets(index, count)); + Future> loadAssets(int index, int count) => _mutex.run(() => _loadAssets(index, count)); Future> _loadAssets(int index, int count) async { if (hasRange(index, count)) { @@ -99,15 +148,18 @@ class TimelineService { : (len > kTimelineAssetLoadBatchSize ? index : index + count - len), ); - final assets = await _assetSource(start, len); - _buffer = assets; + _buffer = await _assetSource(start, len); _bufferOffset = start; return getAssets(index, count); } bool hasRange(int index, int count) => - index >= _bufferOffset && index + count <= _bufferOffset + _buffer.length; + index >= 0 && + index < _totalAssets && + index >= _bufferOffset && + index + count <= _bufferOffset + _buffer.length && + index + count <= _totalAssets; List getAssets(int index, int count) { if (!hasRange(index, count)) { @@ -117,6 +169,20 @@ class TimelineService { return _buffer.slice(start, start + count); } + // Pre-cache assets around the given index for asset viewer + Future preCacheAssets(int index) => _mutex.run(() => _loadAssets(index, math.min(5, _totalAssets - index))); + + BaseAsset getRandomAsset() => _buffer.elementAt(math.Random().nextInt(_buffer.length)); + + BaseAsset getAsset(int index) { + if (!hasRange(index, 1)) { + throw RangeError( + 'TimelineService::getAsset Index $index not in buffer range [$_bufferOffset, ${_bufferOffset + _buffer.length})', + ); + } + return _buffer.elementAt(index - _bufferOffset); + } + Future dispose() async { await _bucketSubscription?.cancel(); _bucketSubscription = null; diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index c8d2e2b624..1524c412f1 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -4,12 +4,38 @@ import 'package:immich_mobile/providers/infrastructure/sync.provider.dart'; import 'package:immich_mobile/utils/isolate.dart'; import 'package:worker_manager/worker_manager.dart'; +typedef SyncCallback = void Function(); +typedef SyncErrorCallback = void Function(String error); + class BackgroundSyncManager { + final SyncCallback? onRemoteSyncStart; + final SyncCallback? onRemoteSyncComplete; + final SyncErrorCallback? onRemoteSyncError; + + final SyncCallback? onLocalSyncStart; + final SyncCallback? onLocalSyncComplete; + final SyncErrorCallback? onLocalSyncError; + + final SyncCallback? onHashingStart; + final SyncCallback? onHashingComplete; + final SyncErrorCallback? onHashingError; + Cancelable? _syncTask; + Cancelable? _syncWebsocketTask; Cancelable? _deviceAlbumSyncTask; Cancelable? _hashTask; - BackgroundSyncManager(); + BackgroundSyncManager({ + this.onRemoteSyncStart, + this.onRemoteSyncComplete, + this.onRemoteSyncError, + this.onLocalSyncStart, + this.onLocalSyncComplete, + this.onLocalSyncError, + this.onHashingStart, + this.onHashingComplete, + this.onHashingError, + }); Future cancel() { final futures = []; @@ -20,6 +46,12 @@ class BackgroundSyncManager { _syncTask?.cancel(); _syncTask = null; + if (_syncWebsocketTask != null) { + futures.add(_syncWebsocketTask!.future); + } + _syncWebsocketTask?.cancel(); + _syncWebsocketTask = null; + return Future.wait(futures); } @@ -29,20 +61,24 @@ class BackgroundSyncManager { return _deviceAlbumSyncTask!.future; } + onLocalSyncStart?.call(); + // We use a ternary operator to avoid [_deviceAlbumSyncTask] from being // captured by the closure passed to [runInIsolateGentle]. _deviceAlbumSyncTask = full ? runInIsolateGentle( - computation: (ref) => - ref.read(localSyncServiceProvider).sync(full: true), + computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true), ) : runInIsolateGentle( - computation: (ref) => - ref.read(localSyncServiceProvider).sync(full: false), + computation: (ref) => ref.read(localSyncServiceProvider).sync(full: false), ); return _deviceAlbumSyncTask!.whenComplete(() { _deviceAlbumSyncTask = null; + onLocalSyncComplete?.call(); + }).catchError((error) { + onLocalSyncError?.call(error.toString()); + _deviceAlbumSyncTask = null; }); } @@ -52,10 +88,17 @@ class BackgroundSyncManager { return _hashTask!.future; } + onHashingStart?.call(); + _hashTask = runInIsolateGentle( computation: (ref) => ref.read(hashServiceProvider).hashAssets(), ); + return _hashTask!.whenComplete(() { + onHashingComplete?.call(); + _hashTask = null; + }).catchError((error) { + onHashingError?.call(error.toString()); _hashTask = null; }); } @@ -65,11 +108,34 @@ class BackgroundSyncManager { return _syncTask!.future; } + onRemoteSyncStart?.call(); + _syncTask = runInIsolateGentle( computation: (ref) => ref.read(syncStreamServiceProvider).sync(), ); return _syncTask!.whenComplete(() { + onRemoteSyncComplete?.call(); + _syncTask = null; + }).catchError((error) { + onRemoteSyncError?.call(error.toString()); _syncTask = null; }); } + + Future syncWebsocketBatch(List batchData) { + if (_syncWebsocketTask != null) { + return _syncWebsocketTask!.future; + } + _syncWebsocketTask = _handleWsAssetUploadReadyV1Batch(batchData); + return _syncWebsocketTask!.whenComplete(() { + _syncWebsocketTask = null; + }); + } } + +Cancelable _handleWsAssetUploadReadyV1Batch( + List batchData, +) => + runInIsolateGentle( + computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData), + ); diff --git a/mobile/lib/domain/utils/event_stream.dart b/mobile/lib/domain/utils/event_stream.dart new file mode 100644 index 0000000000..008ddef183 --- /dev/null +++ b/mobile/lib/domain/utils/event_stream.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +class Event { + const Event(); +} + +class EventStream { + EventStream._(); + + static final EventStream shared = EventStream._(); + + final StreamController _controller = StreamController.broadcast(); + + void emit(Event event) { + _controller.add(event); + } + + Stream where() { + if (T == Event) { + return _controller.stream as Stream; + } + return _controller.stream.where((event) => event is T).cast(); + } + + StreamSubscription listen( + void Function(T event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return where().listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } + + /// Closes the stream controller + void dispose() { + _controller.close(); + } +} diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart index f6d5322752..a7cb612d6e 100644 --- a/mobile/lib/entities/album.entity.dart +++ b/mobile/lib/entities/album.entity.dart @@ -95,13 +95,11 @@ class Album { // accessible in an object freshly created (not loaded from DB) @ignore - Iterable get remoteUsers => sharedUsers.isEmpty - ? (sharedUsers as IsarLinksCommon).addedObjects - : sharedUsers; + Iterable get remoteUsers => + sharedUsers.isEmpty ? (sharedUsers as IsarLinksCommon).addedObjects : sharedUsers; @ignore - Iterable get remoteAssets => - assets.isEmpty ? (assets as IsarLinksCommon).addedObjects : assets; + Iterable get remoteAssets => assets.isEmpty ? (assets as IsarLinksCommon).addedObjects : assets; @override bool operator ==(other) { @@ -164,15 +162,11 @@ class Album { 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; + 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(); + a.thumbnail.value = await db.assets.where().remoteIdEqualTo(dto.albumThumbnailAssetId).findFirst(); } if (dto.albumUsers.isNotEmpty) { final users = await db.users.getAllById( @@ -181,16 +175,14 @@ class Album { a.sharedUsers.addAll(users.cast()); } if (dto.assets.isNotEmpty) { - final assets = - await db.assets.getAllByRemoteId(dto.assets.map((e) => e.id)); + 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'; + String toString() => 'remoteId: $remoteId name: $name description: $description'; } extension AssetsHelper on IsarCollection { diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart index d8d2bd23c3..f5c577a060 100644 --- a/mobile/lib/entities/asset.entity.dart +++ b/mobile/lib/entities/asset.entity.dart @@ -4,8 +4,7 @@ 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/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'; @@ -32,18 +31,14 @@ class Asset { width = remote.exifInfo?.exifImageWidth?.toInt(), livePhotoVideoId = remote.livePhotoVideoId, ownerId = fastHash(remote.ownerId), - exifInfo = remote.exifInfo == null - ? null - : ExifDtoConverter.fromDto(remote.exifInfo!), + 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, + stackPrimaryAssetId = remote.stack?.primaryAssetId == remote.id ? null : remote.stack?.primaryAssetId, stackCount = remote.stack?.assetCount ?? 0, stackId = remote.stack?.id, thumbhash = remote.thumbhash, @@ -108,8 +103,7 @@ class Asset { throw Exception('Asset $fileName has no local data'); } - final updatedLocal = - _didUpdateLocal ? local : await local.obtainForNewProperties(); + final updatedLocal = _didUpdateLocal ? local : await local.obtainForNewProperties(); if (updatedLocal == null) { throw Exception('Could not fetch local data for $fileName'); } @@ -185,10 +179,7 @@ class Asset { final orientatedWidth = this.orientatedWidth; final orientatedHeight = this.orientatedHeight; - if (orientatedWidth != null && - orientatedHeight != null && - orientatedWidth > 0 && - orientatedHeight > 0) { + if (orientatedWidth != null && orientatedHeight != null && orientatedWidth > 0 && orientatedHeight > 0) { return orientatedWidth.toDouble() / orientatedHeight.toDouble(); } @@ -389,8 +380,7 @@ class Asset { // 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, + stackPrimaryAssetId: stackPrimaryAssetId == remoteId ? null : stackPrimaryAssetId, stackCount: stackCount, isFavorite: isFavorite, isArchived: isArchived, @@ -410,9 +400,7 @@ class Asset { // 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, + stackPrimaryAssetId: a.stackPrimaryAssetId == a.remoteId ? null : a.stackPrimaryAssetId, stackCount: a.stackCount, // isFavorite + isArchived are not set by device-only assets isFavorite: a.isFavorite, @@ -428,8 +416,7 @@ class Asset { localId: localId ?? a.localId, width: width ?? a.width, height: height ?? a.height, - exifInfo: exifInfo ?? - a.exifInfo?.copyWith(assetId: id), // updated to use assetId + exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id), // updated to use assetId ); } } @@ -491,18 +478,15 @@ class Asset { Future put(Isar db) async { await db.assets.put(this); if (exifInfo != null) { - await db.exifInfos - .put(entity.ExifInfo.fromDto(exifInfo!.copyWith(assetId: id))); + 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 compareByLocalId(Asset a, Asset b) => compareToNullable(a.localId, b.localId); - static int compareByChecksum(Asset a, Asset b) => - a.checksum.compareTo(b.checksum); + 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); @@ -554,18 +538,12 @@ class Asset { }"""; } - static getVisibility(AssetVisibility visibility) { - switch (visibility) { - case AssetVisibility.timeline: - return AssetVisibilityEnum.timeline; - case AssetVisibility.archive: - return AssetVisibilityEnum.archive; - case AssetVisibility.hidden: - return AssetVisibilityEnum.hidden; - case AssetVisibility.locked: - return AssetVisibilityEnum.locked; - } - } + static getVisibility(AssetVisibility visibility) => switch (visibility) { + AssetVisibility.archive => AssetVisibilityEnum.archive, + AssetVisibility.hidden => AssetVisibilityEnum.hidden, + AssetVisibility.locked => AssetVisibilityEnum.locked, + AssetVisibility.timeline || _ => AssetVisibilityEnum.timeline, + }; } enum AssetType { @@ -595,16 +573,11 @@ enum AssetState { } 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(); + 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, diff --git a/mobile/lib/entities/backup_album.entity.dart b/mobile/lib/entities/backup_album.entity.dart index 4d4d7b3aa3..1e96c0452e 100644 --- a/mobile/lib/entities/backup_album.entity.dart +++ b/mobile/lib/entities/backup_album.entity.dart @@ -13,6 +13,18 @@ class BackupAlbum { 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 { diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index ed955352e2..7b59e119d6 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -11,13 +11,13 @@ class SSLClientCertStoreVal { final Uint8List data; final String? password; - SSLClientCertStoreVal(this.data, this.password); + const SSLClientCertStoreVal(this.data, this.password); - void save() { + Future save() async { final b64Str = base64Encode(data); - Store.put(StoreKey.sslClientCertData, b64Str); + await Store.put(StoreKey.sslClientCertData, b64Str); if (password != null) { - Store.put(StoreKey.sslClientPasswd, password!); + await Store.put(StoreKey.sslClientPasswd, password!); } } @@ -31,8 +31,8 @@ class SSLClientCertStoreVal { return SSLClientCertStoreVal(certData, passwd); } - static void delete() { - Store.delete(StoreKey.sslClientCertData); - Store.delete(StoreKey.sslClientPasswd); + static Future delete() async { + await Store.delete(StoreKey.sslClientCertData); + await Store.delete(StoreKey.sslClientPasswd); } } diff --git a/mobile/lib/extensions/asyncvalue_extensions.dart b/mobile/lib/extensions/asyncvalue_extensions.dart index 554c3a8a8a..c76d7e48d8 100644 --- a/mobile/lib/extensions/asyncvalue_extensions.dart +++ b/mobile/lib/extensions/asyncvalue_extensions.dart @@ -22,15 +22,13 @@ extension LogOnError on AsyncValue { } if (!skip) { - return onLoading?.call() ?? - const Center(child: ImmichLoadingIndicator()); + return onLoading?.call() ?? const Center(child: ImmichLoadingIndicator()); } } if (hasError && !hasValue) { _asyncErrorLogger.severe('Could not load value', error, stackTrace); - return onError?.call(error, stackTrace) ?? - ScaffoldErrorBody(errorMsg: error?.toString()); + return onError?.call(error, stackTrace) ?? ScaffoldErrorBody(errorMsg: error?.toString()); } return onData(requireValue); diff --git a/mobile/lib/extensions/build_context_extensions.dart b/mobile/lib/extensions/build_context_extensions.dart index 69a9c3b347..a624337954 100644 --- a/mobile/lib/extensions/build_context_extensions.dart +++ b/mobile/lib/extensions/build_context_extensions.dart @@ -33,6 +33,10 @@ extension ContextHelper on BuildContext { // Returns the current Primary color of the Theme Color get primaryColor => themeData.colorScheme.primary; + Color get logoYellow => const Color.fromARGB(255, 255, 184, 0); + Color get logoRed => const Color.fromARGB(255, 230, 65, 30); + Color get logoPink => const Color.fromARGB(255, 222, 127, 179); + Color get logoGreen => const Color.fromARGB(255, 49, 164, 82); // Returns the Scaffold background color of the Theme Color get scaffoldBackgroundColor => colorScheme.surface; @@ -56,6 +60,5 @@ extension ContextHelper on BuildContext { FocusScopeNode get focusScope => FocusScope.of(this); // Show SnackBars from the current context - void showSnackBar(SnackBar snackBar) => - ScaffoldMessenger.of(this).showSnackBar(snackBar); + void showSnackBar(SnackBar snackBar) => ScaffoldMessenger.of(this).showSnackBar(snackBar); } diff --git a/mobile/lib/extensions/datetime_extensions.dart b/mobile/lib/extensions/datetime_extensions.dart index 14d89e2755..7e54980270 100644 --- a/mobile/lib/extensions/datetime_extensions.dart +++ b/mobile/lib/extensions/datetime_extensions.dart @@ -1,3 +1,6 @@ +import 'dart:ui'; +import 'package:easy_localization/easy_localization.dart'; + extension TimeAgoExtension on DateTime { /// Displays the time difference of this [DateTime] object to the current time as a [String] String timeAgo({bool numericDates = true}) { @@ -35,3 +38,54 @@ extension TimeAgoExtension on DateTime { return '${(difference.inDays / 365).floor()} years ago'; } } + +/// Extension to format date ranges according to UI requirements +extension DateRangeFormatting on DateTime { + /// Formats a date range according to specific rules: + /// - Single date of this year: "Aug 28" + /// - Single date of other year: "Aug 28, 2023" + /// - Date range of this year: "Mar 23-May 31" + /// - Date range of other year: "Aug 28 - Sep 30, 2023" + /// - Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022" + static String formatDateRange( + DateTime startDate, + DateTime endDate, + Locale? locale, + ) { + final now = DateTime.now(); + final currentYear = now.year; + final localeString = locale?.toString() ?? 'en_US'; + + // Check if it's a single date (same day) + if (startDate.year == endDate.year && startDate.month == endDate.month && startDate.day == endDate.day) { + if (startDate.year == currentYear) { + // Single date of this year: "Aug 28" + return DateFormat.MMMd(localeString).format(startDate); + } else { + // Single date of other year: "Aug 28, 2023" + return DateFormat.yMMMd(localeString).format(startDate); + } + } + + // It's a date range + if (startDate.year == endDate.year) { + // Same year + if (startDate.year == currentYear) { + // Date range of this year: "Mar 23-May 31" + final startFormatted = DateFormat.MMMd(localeString).format(startDate); + final endFormatted = DateFormat.MMMd(localeString).format(endDate); + return '$startFormatted - $endFormatted'; + } else { + // Date range of other year: "Aug 28 - Sep 30, 2023" + final startFormatted = DateFormat.MMMd(localeString).format(startDate); + final endFormatted = DateFormat.MMMd(localeString).format(endDate); + return '$startFormatted - $endFormatted, ${startDate.year}'; + } + } else { + // Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022" + final startFormatted = DateFormat.yMMMd(localeString).format(startDate); + final endFormatted = DateFormat.yMMMd(localeString).format(endDate); + return '$startFormatted - $endFormatted'; + } + } +} diff --git a/mobile/lib/extensions/duration_extensions.dart b/mobile/lib/extensions/duration_extensions.dart index ca5ba8310c..492627a727 100644 --- a/mobile/lib/extensions/duration_extensions.dart +++ b/mobile/lib/extensions/duration_extensions.dart @@ -3,3 +3,15 @@ extension TZOffsetExtension on Duration { String formatAsOffset() => "${isNegative ? '-' : '+'}${inHours.abs().toString().padLeft(2, '0')}:${inMinutes.abs().remainder(60).toString().padLeft(2, '0')}"; } + +extension DurationFormatExtension on Duration { + String format() { + final seconds = inSeconds.remainder(60).toString().padLeft(2, '0'); + final minutes = inMinutes.remainder(60).toString().padLeft(2, '0'); + if (inHours == 0) { + return "$minutes:$seconds"; + } + final hours = inHours.toString().padLeft(2, '0'); + return "$hours:$minutes:$seconds"; + } +} diff --git a/mobile/lib/extensions/scroll_extensions.dart b/mobile/lib/extensions/scroll_extensions.dart index 5bbd73163a..2752d0b77a 100644 --- a/mobile/lib/extensions/scroll_extensions.dart +++ b/mobile/lib/extensions/scroll_extensions.dart @@ -11,9 +11,9 @@ class FastScrollPhysics extends ScrollPhysics { @override SpringDescription get spring => const SpringDescription( - mass: 40, - stiffness: 100, - damping: 1, + mass: 1, + stiffness: 402.49984375, + damping: 40, ); } @@ -31,8 +31,8 @@ class FastClampingScrollPhysics extends ClampingScrollPhysics { // can briefly be seen and cause a flicker effect if the video begins to initialize // before the animation finishes - probably a bug in PhotoViewGallery's animation handling // Making the animation faster is not just stylistic, but also helps to avoid this flicker - mass: 80, - stiffness: 100, - damping: 1, + mass: 1, + stiffness: 1601.2499609375, + damping: 80, ); } diff --git a/mobile/lib/extensions/string_extensions.dart b/mobile/lib/extensions/string_extensions.dart index 67411013ee..65660c04ef 100644 --- a/mobile/lib/extensions/string_extensions.dart +++ b/mobile/lib/extensions/string_extensions.dart @@ -12,9 +12,7 @@ extension DurationExtension on String { /// Parses and returns the string of format HH:MM:SS as a duration object else null Duration? toDuration() { try { - final parts = split(':') - .map((e) => double.parse(e).toInt()) - .toList(growable: false); + final parts = split(':').map((e) => double.parse(e).toInt()).toList(growable: false); return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]); } catch (e) { return null; diff --git a/mobile/lib/extensions/theme_extensions.dart b/mobile/lib/extensions/theme_extensions.dart index b81e4476e0..6da8ac1fe6 100644 --- a/mobile/lib/extensions/theme_extensions.dart +++ b/mobile/lib/extensions/theme_extensions.dart @@ -2,9 +2,7 @@ import 'package:flutter/material.dart'; extension ImmichColorSchemeExtensions on ColorScheme { bool get _isDarkMode => brightness == Brightness.dark; - Color get onSurfaceSecondary => _isDarkMode - ? onSurface.darken(amount: .3) - : onSurface.lighten(amount: .3); + Color get onSurfaceSecondary => _isDarkMode ? onSurface.darken(amount: .3) : onSurface.lighten(amount: .3); } extension ColorExtensions on Color { diff --git a/mobile/lib/extensions/translate_extensions.dart b/mobile/lib/extensions/translate_extensions.dart index 122830843d..d8a2810e79 100644 --- a/mobile/lib/extensions/translate_extensions.dart +++ b/mobile/lib/extensions/translate_extensions.dart @@ -40,8 +40,7 @@ String _translateHelper( try { final translatedMessage = key.tr(context: context); return args != null - ? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en') - .format(args) + ? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en').format(args) : translatedMessage; } catch (e) { debugPrint('Translation failed for key "$key". Error: $e'); diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.dart b/mobile/lib/infrastructure/entities/asset_face.entity.dart new file mode 100644 index 0000000000..5f793030c3 --- /dev/null +++ b/mobile/lib/infrastructure/entities/asset_face.entity.dart @@ -0,0 +1,31 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/person.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class AssetFaceEntity extends Table with DriftDefaultsMixin { + const AssetFaceEntity(); + + TextColumn get id => text()(); + + TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get personId => text().nullable().references(PersonEntity, #id, onDelete: KeyAction.setNull)(); + + IntColumn get imageWidth => integer()(); + + IntColumn get imageHeight => integer()(); + + IntColumn get boundingBoxX1 => integer()(); + + IntColumn get boundingBoxY1 => integer()(); + + IntColumn get boundingBoxX2 => integer()(); + + IntColumn get boundingBoxY2 => integer()(); + + TextColumn get sourceType => text()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart b/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart new file mode 100644 index 0000000000..140af60de1 --- /dev/null +++ b/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart @@ -0,0 +1,1013 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart' + as i2; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i3; +import 'package:drift/internal/modular.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' + as i5; + +typedef $$AssetFaceEntityTableCreateCompanionBuilder + = i1.AssetFaceEntityCompanion Function({ + required String id, + required String assetId, + i0.Value personId, + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, +}); +typedef $$AssetFaceEntityTableUpdateCompanionBuilder + = i1.AssetFaceEntityCompanion Function({ + i0.Value id, + i0.Value assetId, + i0.Value personId, + i0.Value imageWidth, + i0.Value imageHeight, + i0.Value boundingBoxX1, + i0.Value boundingBoxY1, + i0.Value boundingBoxX2, + i0.Value boundingBoxY2, + i0.Value sourceType, +}); + +final class $$AssetFaceEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, i1.$AssetFaceEntityTable, i1.AssetFaceEntityData> { + $$AssetFaceEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet('asset_face_entity') + .assetId, + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .id)); + + i3.$$RemoteAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i3 + .$$RemoteAssetEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('remote_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i5.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('person_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet('asset_face_entity') + .personId, + i4.ReadDatabaseContainer(db) + .resultSet('person_entity') + .id)); + + i5.$$PersonEntityTableProcessedTableManager? get personId { + final $_column = $_itemColumn('person_id'); + if ($_column == null) return null; + final manager = i5 + .$$PersonEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('person_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_personIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$AssetFaceEntityTableFilterComposer + extends i0.Composer { + $$AssetFaceEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get imageWidth => $composableBuilder( + column: $table.imageWidth, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get imageHeight => $composableBuilder( + column: $table.imageHeight, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get boundingBoxX1 => $composableBuilder( + column: $table.boundingBoxX1, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get boundingBoxY1 => $composableBuilder( + column: $table.boundingBoxY1, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get boundingBoxX2 => $composableBuilder( + column: $table.boundingBoxX2, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get boundingBoxY2 => $composableBuilder( + column: $table.boundingBoxY2, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get sourceType => $composableBuilder( + column: $table.sourceType, builder: (column) => i0.ColumnFilters(column)); + + i3.$$RemoteAssetEntityTableFilterComposer get assetId { + final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$PersonEntityTableFilterComposer get personId { + final i5.$$PersonEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.personId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('person_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$PersonEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('person_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$AssetFaceEntityTableOrderingComposer + extends i0.Composer { + $$AssetFaceEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get imageWidth => $composableBuilder( + column: $table.imageWidth, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get imageHeight => $composableBuilder( + column: $table.imageHeight, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get boundingBoxX1 => $composableBuilder( + column: $table.boundingBoxX1, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get boundingBoxY1 => $composableBuilder( + column: $table.boundingBoxY1, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get boundingBoxX2 => $composableBuilder( + column: $table.boundingBoxX2, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get boundingBoxY2 => $composableBuilder( + column: $table.boundingBoxY2, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get sourceType => $composableBuilder( + column: $table.sourceType, + builder: (column) => i0.ColumnOrderings(column)); + + i3.$$RemoteAssetEntityTableOrderingComposer get assetId { + final i3.$$RemoteAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$PersonEntityTableOrderingComposer get personId { + final i5.$$PersonEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.personId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('person_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$PersonEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('person_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$AssetFaceEntityTableAnnotationComposer + extends i0.Composer { + $$AssetFaceEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get imageWidth => $composableBuilder( + column: $table.imageWidth, builder: (column) => column); + + i0.GeneratedColumn get imageHeight => $composableBuilder( + column: $table.imageHeight, builder: (column) => column); + + i0.GeneratedColumn get boundingBoxX1 => $composableBuilder( + column: $table.boundingBoxX1, builder: (column) => column); + + i0.GeneratedColumn get boundingBoxY1 => $composableBuilder( + column: $table.boundingBoxY1, builder: (column) => column); + + i0.GeneratedColumn get boundingBoxX2 => $composableBuilder( + column: $table.boundingBoxX2, builder: (column) => column); + + i0.GeneratedColumn get boundingBoxY2 => $composableBuilder( + column: $table.boundingBoxY2, builder: (column) => column); + + i0.GeneratedColumn get sourceType => $composableBuilder( + column: $table.sourceType, builder: (column) => column); + + i3.$$RemoteAssetEntityTableAnnotationComposer get assetId { + final i3.$$RemoteAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$PersonEntityTableAnnotationComposer get personId { + final i5.$$PersonEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.personId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('person_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$PersonEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('person_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$AssetFaceEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$AssetFaceEntityTable, + i1.AssetFaceEntityData, + i1.$$AssetFaceEntityTableFilterComposer, + i1.$$AssetFaceEntityTableOrderingComposer, + i1.$$AssetFaceEntityTableAnnotationComposer, + $$AssetFaceEntityTableCreateCompanionBuilder, + $$AssetFaceEntityTableUpdateCompanionBuilder, + (i1.AssetFaceEntityData, i1.$$AssetFaceEntityTableReferences), + i1.AssetFaceEntityData, + i0.PrefetchHooks Function({bool assetId, bool personId})> { + $$AssetFaceEntityTableTableManager( + i0.GeneratedDatabase db, i1.$AssetFaceEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$AssetFaceEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$AssetFaceEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => i1 + .$$AssetFaceEntityTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value assetId = const i0.Value.absent(), + i0.Value personId = const i0.Value.absent(), + i0.Value imageWidth = const i0.Value.absent(), + i0.Value imageHeight = const i0.Value.absent(), + i0.Value boundingBoxX1 = const i0.Value.absent(), + i0.Value boundingBoxY1 = const i0.Value.absent(), + i0.Value boundingBoxX2 = const i0.Value.absent(), + i0.Value boundingBoxY2 = const i0.Value.absent(), + i0.Value sourceType = const i0.Value.absent(), + }) => + i1.AssetFaceEntityCompanion( + id: id, + assetId: assetId, + personId: personId, + imageWidth: imageWidth, + imageHeight: imageHeight, + boundingBoxX1: boundingBoxX1, + boundingBoxY1: boundingBoxY1, + boundingBoxX2: boundingBoxX2, + boundingBoxY2: boundingBoxY2, + sourceType: sourceType, + ), + createCompanionCallback: ({ + required String id, + required String assetId, + i0.Value personId = const i0.Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) => + i1.AssetFaceEntityCompanion.insert( + id: id, + assetId: assetId, + personId: personId, + imageWidth: imageWidth, + imageHeight: imageHeight, + boundingBoxX1: boundingBoxX1, + boundingBoxY1: boundingBoxY1, + boundingBoxX2: boundingBoxX2, + boundingBoxY2: boundingBoxY2, + sourceType: sourceType, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$AssetFaceEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({assetId = false, personId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (assetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: + i1.$$AssetFaceEntityTableReferences._assetIdTable(db), + referencedColumn: i1.$$AssetFaceEntityTableReferences + ._assetIdTable(db) + .id, + ) as T; + } + if (personId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.personId, + referencedTable: + i1.$$AssetFaceEntityTableReferences._personIdTable(db), + referencedColumn: i1.$$AssetFaceEntityTableReferences + ._personIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$AssetFaceEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$AssetFaceEntityTable, + i1.AssetFaceEntityData, + i1.$$AssetFaceEntityTableFilterComposer, + i1.$$AssetFaceEntityTableOrderingComposer, + i1.$$AssetFaceEntityTableAnnotationComposer, + $$AssetFaceEntityTableCreateCompanionBuilder, + $$AssetFaceEntityTableUpdateCompanionBuilder, + (i1.AssetFaceEntityData, i1.$$AssetFaceEntityTableReferences), + i1.AssetFaceEntityData, + i0.PrefetchHooks Function({bool assetId, bool personId})>; + +class $AssetFaceEntityTable extends i2.AssetFaceEntity + with i0.TableInfo<$AssetFaceEntityTable, i1.AssetFaceEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $AssetFaceEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _assetIdMeta = + const i0.VerificationMeta('assetId'); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _personIdMeta = + const i0.VerificationMeta('personId'); + @override + late final i0.GeneratedColumn personId = i0.GeneratedColumn( + 'person_id', aliasedName, true, + type: i0.DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL')); + static const i0.VerificationMeta _imageWidthMeta = + const i0.VerificationMeta('imageWidth'); + @override + late final i0.GeneratedColumn imageWidth = i0.GeneratedColumn( + 'image_width', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true); + static const i0.VerificationMeta _imageHeightMeta = + const i0.VerificationMeta('imageHeight'); + @override + late final i0.GeneratedColumn imageHeight = i0.GeneratedColumn( + 'image_height', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true); + static const i0.VerificationMeta _boundingBoxX1Meta = + const i0.VerificationMeta('boundingBoxX1'); + @override + late final i0.GeneratedColumn boundingBoxX1 = i0.GeneratedColumn( + 'bounding_box_x1', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true); + static const i0.VerificationMeta _boundingBoxY1Meta = + const i0.VerificationMeta('boundingBoxY1'); + @override + late final i0.GeneratedColumn boundingBoxY1 = i0.GeneratedColumn( + 'bounding_box_y1', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true); + static const i0.VerificationMeta _boundingBoxX2Meta = + const i0.VerificationMeta('boundingBoxX2'); + @override + late final i0.GeneratedColumn boundingBoxX2 = i0.GeneratedColumn( + 'bounding_box_x2', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true); + static const i0.VerificationMeta _boundingBoxY2Meta = + const i0.VerificationMeta('boundingBoxY2'); + @override + late final i0.GeneratedColumn boundingBoxY2 = i0.GeneratedColumn( + 'bounding_box_y2', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true); + static const i0.VerificationMeta _sourceTypeMeta = + const i0.VerificationMeta('sourceType'); + @override + late final i0.GeneratedColumn sourceType = i0.GeneratedColumn( + 'source_type', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('asset_id')) { + context.handle(_assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + if (data.containsKey('person_id')) { + context.handle(_personIdMeta, + personId.isAcceptableOrUnknown(data['person_id']!, _personIdMeta)); + } + if (data.containsKey('image_width')) { + context.handle( + _imageWidthMeta, + imageWidth.isAcceptableOrUnknown( + data['image_width']!, _imageWidthMeta)); + } else if (isInserting) { + context.missing(_imageWidthMeta); + } + if (data.containsKey('image_height')) { + context.handle( + _imageHeightMeta, + imageHeight.isAcceptableOrUnknown( + data['image_height']!, _imageHeightMeta)); + } else if (isInserting) { + context.missing(_imageHeightMeta); + } + if (data.containsKey('bounding_box_x1')) { + context.handle( + _boundingBoxX1Meta, + boundingBoxX1.isAcceptableOrUnknown( + data['bounding_box_x1']!, _boundingBoxX1Meta)); + } else if (isInserting) { + context.missing(_boundingBoxX1Meta); + } + if (data.containsKey('bounding_box_y1')) { + context.handle( + _boundingBoxY1Meta, + boundingBoxY1.isAcceptableOrUnknown( + data['bounding_box_y1']!, _boundingBoxY1Meta)); + } else if (isInserting) { + context.missing(_boundingBoxY1Meta); + } + if (data.containsKey('bounding_box_x2')) { + context.handle( + _boundingBoxX2Meta, + boundingBoxX2.isAcceptableOrUnknown( + data['bounding_box_x2']!, _boundingBoxX2Meta)); + } else if (isInserting) { + context.missing(_boundingBoxX2Meta); + } + if (data.containsKey('bounding_box_y2')) { + context.handle( + _boundingBoxY2Meta, + boundingBoxY2.isAcceptableOrUnknown( + data['bounding_box_y2']!, _boundingBoxY2Meta)); + } else if (isInserting) { + context.missing(_boundingBoxY2Meta); + } + if (data.containsKey('source_type')) { + context.handle( + _sourceTypeMeta, + sourceType.isAcceptableOrUnknown( + data['source_type']!, _sourceTypeMeta)); + } else if (isInserting) { + context.missing(_sourceTypeMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.AssetFaceEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + assetId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + personId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}person_id']), + imageWidth: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}image_width'])!, + imageHeight: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}image_height'])!, + boundingBoxX1: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}bounding_box_x1'])!, + boundingBoxY1: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}bounding_box_y1'])!, + boundingBoxX2: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}bounding_box_x2'])!, + boundingBoxY2: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}bounding_box_y2'])!, + sourceType: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}source_type'])!, + ); + } + + @override + $AssetFaceEntityTable createAlias(String alias) { + return $AssetFaceEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData( + {required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['asset_id'] = i0.Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = i0.Variable(personId); + } + map['image_width'] = i0.Variable(imageWidth); + map['image_height'] = i0.Variable(imageHeight); + map['bounding_box_x1'] = i0.Variable(boundingBoxX1); + map['bounding_box_y1'] = i0.Variable(boundingBoxY1); + map['bounding_box_x2'] = i0.Variable(boundingBoxX2); + map['bounding_box_y2'] = i0.Variable(boundingBoxY2); + map['source_type'] = i0.Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + i1.AssetFaceEntityData copyWith( + {String? id, + String? assetId, + i0.Value personId = const i0.Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType}) => + i1.AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(i1.AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: + data.imageWidth.present ? data.imageWidth.value : this.imageWidth, + imageHeight: + data.imageHeight.present ? data.imageHeight.value : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: + data.sourceType.present ? data.sourceType.value : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion + extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value assetId; + final i0.Value personId; + final i0.Value imageWidth; + final i0.Value imageHeight; + final i0.Value boundingBoxX1; + final i0.Value boundingBoxY1; + final i0.Value boundingBoxX2; + final i0.Value boundingBoxY2; + final i0.Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const i0.Value.absent(), + this.assetId = const i0.Value.absent(), + this.personId = const i0.Value.absent(), + this.imageWidth = const i0.Value.absent(), + this.imageHeight = const i0.Value.absent(), + this.boundingBoxX1 = const i0.Value.absent(), + this.boundingBoxY1 = const i0.Value.absent(), + this.boundingBoxX2 = const i0.Value.absent(), + this.boundingBoxY2 = const i0.Value.absent(), + this.sourceType = const i0.Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const i0.Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = i0.Value(id), + assetId = i0.Value(assetId), + imageWidth = i0.Value(imageWidth), + imageHeight = i0.Value(imageHeight), + boundingBoxX1 = i0.Value(boundingBoxX1), + boundingBoxY1 = i0.Value(boundingBoxY1), + boundingBoxX2 = i0.Value(boundingBoxX2), + boundingBoxY2 = i0.Value(boundingBoxY2), + sourceType = i0.Value(sourceType); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? assetId, + i0.Expression? personId, + i0.Expression? imageWidth, + i0.Expression? imageHeight, + i0.Expression? boundingBoxX1, + i0.Expression? boundingBoxY1, + i0.Expression? boundingBoxX2, + i0.Expression? boundingBoxY2, + i0.Expression? sourceType, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + i1.AssetFaceEntityCompanion copyWith( + {i0.Value? id, + i0.Value? assetId, + i0.Value? personId, + i0.Value? imageWidth, + i0.Value? imageHeight, + i0.Value? boundingBoxX1, + i0.Value? boundingBoxY1, + i0.Value? boundingBoxX2, + i0.Value? boundingBoxY2, + i0.Value? sourceType}) { + return i1.AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = i0.Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = i0.Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = i0.Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = i0.Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = i0.Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = i0.Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = i0.Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = i0.Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 11730b7761..dae5486ab1 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -1,5 +1,6 @@ import 'package:drift/drift.dart' hide Query; import 'package:immich_mobile/domain/models/exif.model.dart' as domain; +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'; @@ -97,8 +98,7 @@ class ExifInfo { class RemoteExifEntity extends Table with DriftDefaultsMixin { const RemoteExifEntity(); - TextColumn get assetId => - text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); TextColumn get city => text().nullable()(); @@ -116,15 +116,15 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin { TextColumn get exposureTime => text().nullable()(); - IntColumn get fNumber => integer().nullable()(); + RealColumn get fNumber => real().nullable()(); IntColumn get fileSize => integer().nullable()(); - IntColumn get focalLength => integer().nullable()(); + RealColumn get focalLength => real().nullable()(); - IntColumn get latitude => integer().nullable()(); + RealColumn get latitude => real().nullable()(); - IntColumn get longitude => integer().nullable()(); + RealColumn get longitude => real().nullable()(); IntColumn get iso => integer().nullable()(); @@ -132,6 +132,8 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin { TextColumn get model => text().nullable()(); + TextColumn get lens => text().nullable()(); + TextColumn get orientation => text().nullable()(); TextColumn get timeZone => text().nullable()(); @@ -143,3 +145,27 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin { @override Set get primaryKey => {assetId}; } + +extension RemoteExifEntityDataDomainEx on RemoteExifEntityData { + domain.ExifInfo toDto() => domain.ExifInfo( + fileSize: fileSize, + dateTimeOriginal: dateTimeOriginal, + timeZone: timeZone, + make: make, + model: model, + iso: iso, + city: city, + state: state, + country: country, + description: description, + orientation: orientation, + latitude: latitude, + longitude: longitude, + f: fNumber?.toDouble(), + mm: focalLength?.toDouble(), + lens: lens, + width: width?.toDouble(), + height: height?.toDouble(), + isFlipped: ExifDtoConverter.isOrientationFlipped(orientation), + ); +} diff --git a/mobile/lib/infrastructure/entities/exif.entity.drift.dart b/mobile/lib/infrastructure/entities/exif.entity.drift.dart index 10025d9cb8..10712948ea 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.drift.dart @@ -19,14 +19,15 @@ typedef $$RemoteExifEntityTableCreateCompanionBuilder i0.Value height, i0.Value width, i0.Value exposureTime, - i0.Value fNumber, + i0.Value fNumber, i0.Value fileSize, - i0.Value focalLength, - i0.Value latitude, - i0.Value longitude, + i0.Value focalLength, + i0.Value latitude, + i0.Value longitude, i0.Value iso, i0.Value make, i0.Value model, + i0.Value lens, i0.Value orientation, i0.Value timeZone, i0.Value rating, @@ -43,14 +44,15 @@ typedef $$RemoteExifEntityTableUpdateCompanionBuilder i0.Value height, i0.Value width, i0.Value exposureTime, - i0.Value fNumber, + i0.Value fNumber, i0.Value fileSize, - i0.Value focalLength, - i0.Value latitude, - i0.Value longitude, + i0.Value focalLength, + i0.Value latitude, + i0.Value longitude, i0.Value iso, i0.Value make, i0.Value model, + i0.Value lens, i0.Value orientation, i0.Value timeZone, i0.Value rating, @@ -125,20 +127,20 @@ class $$RemoteExifEntityTableFilterComposer column: $table.exposureTime, builder: (column) => i0.ColumnFilters(column)); - i0.ColumnFilters get fNumber => $composableBuilder( + i0.ColumnFilters get fNumber => $composableBuilder( column: $table.fNumber, builder: (column) => i0.ColumnFilters(column)); i0.ColumnFilters get fileSize => $composableBuilder( column: $table.fileSize, builder: (column) => i0.ColumnFilters(column)); - i0.ColumnFilters get focalLength => $composableBuilder( + i0.ColumnFilters get focalLength => $composableBuilder( column: $table.focalLength, builder: (column) => i0.ColumnFilters(column)); - i0.ColumnFilters get latitude => $composableBuilder( + i0.ColumnFilters get latitude => $composableBuilder( column: $table.latitude, builder: (column) => i0.ColumnFilters(column)); - i0.ColumnFilters get longitude => $composableBuilder( + i0.ColumnFilters get longitude => $composableBuilder( column: $table.longitude, builder: (column) => i0.ColumnFilters(column)); i0.ColumnFilters get iso => $composableBuilder( @@ -150,6 +152,9 @@ class $$RemoteExifEntityTableFilterComposer i0.ColumnFilters get model => $composableBuilder( column: $table.model, builder: (column) => i0.ColumnFilters(column)); + i0.ColumnFilters get lens => $composableBuilder( + column: $table.lens, builder: (column) => i0.ColumnFilters(column)); + i0.ColumnFilters get orientation => $composableBuilder( column: $table.orientation, builder: (column) => i0.ColumnFilters(column)); @@ -223,20 +228,20 @@ class $$RemoteExifEntityTableOrderingComposer column: $table.exposureTime, builder: (column) => i0.ColumnOrderings(column)); - i0.ColumnOrderings get fNumber => $composableBuilder( + i0.ColumnOrderings get fNumber => $composableBuilder( column: $table.fNumber, builder: (column) => i0.ColumnOrderings(column)); i0.ColumnOrderings get fileSize => $composableBuilder( column: $table.fileSize, builder: (column) => i0.ColumnOrderings(column)); - i0.ColumnOrderings get focalLength => $composableBuilder( + i0.ColumnOrderings get focalLength => $composableBuilder( column: $table.focalLength, builder: (column) => i0.ColumnOrderings(column)); - i0.ColumnOrderings get latitude => $composableBuilder( + i0.ColumnOrderings get latitude => $composableBuilder( column: $table.latitude, builder: (column) => i0.ColumnOrderings(column)); - i0.ColumnOrderings get longitude => $composableBuilder( + i0.ColumnOrderings get longitude => $composableBuilder( column: $table.longitude, builder: (column) => i0.ColumnOrderings(column)); @@ -249,6 +254,9 @@ class $$RemoteExifEntityTableOrderingComposer i0.ColumnOrderings get model => $composableBuilder( column: $table.model, builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get lens => $composableBuilder( + column: $table.lens, builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get orientation => $composableBuilder( column: $table.orientation, builder: (column) => i0.ColumnOrderings(column)); @@ -321,19 +329,19 @@ class $$RemoteExifEntityTableAnnotationComposer i0.GeneratedColumn get exposureTime => $composableBuilder( column: $table.exposureTime, builder: (column) => column); - i0.GeneratedColumn get fNumber => + i0.GeneratedColumn get fNumber => $composableBuilder(column: $table.fNumber, builder: (column) => column); i0.GeneratedColumn get fileSize => $composableBuilder(column: $table.fileSize, builder: (column) => column); - i0.GeneratedColumn get focalLength => $composableBuilder( + i0.GeneratedColumn get focalLength => $composableBuilder( column: $table.focalLength, builder: (column) => column); - i0.GeneratedColumn get latitude => + i0.GeneratedColumn get latitude => $composableBuilder(column: $table.latitude, builder: (column) => column); - i0.GeneratedColumn get longitude => + i0.GeneratedColumn get longitude => $composableBuilder(column: $table.longitude, builder: (column) => column); i0.GeneratedColumn get iso => @@ -345,6 +353,9 @@ class $$RemoteExifEntityTableAnnotationComposer i0.GeneratedColumn get model => $composableBuilder(column: $table.model, builder: (column) => column); + i0.GeneratedColumn get lens => + $composableBuilder(column: $table.lens, builder: (column) => column); + i0.GeneratedColumn get orientation => $composableBuilder( column: $table.orientation, builder: (column) => column); @@ -416,14 +427,15 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager< i0.Value height = const i0.Value.absent(), i0.Value width = const i0.Value.absent(), i0.Value exposureTime = const i0.Value.absent(), - i0.Value fNumber = const i0.Value.absent(), + i0.Value fNumber = const i0.Value.absent(), i0.Value fileSize = const i0.Value.absent(), - i0.Value focalLength = const i0.Value.absent(), - i0.Value latitude = const i0.Value.absent(), - i0.Value longitude = const i0.Value.absent(), + i0.Value focalLength = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), i0.Value iso = const i0.Value.absent(), i0.Value make = const i0.Value.absent(), i0.Value model = const i0.Value.absent(), + i0.Value lens = const i0.Value.absent(), i0.Value orientation = const i0.Value.absent(), i0.Value timeZone = const i0.Value.absent(), i0.Value rating = const i0.Value.absent(), @@ -447,6 +459,7 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager< iso: iso, make: make, model: model, + lens: lens, orientation: orientation, timeZone: timeZone, rating: rating, @@ -462,14 +475,15 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager< i0.Value height = const i0.Value.absent(), i0.Value width = const i0.Value.absent(), i0.Value exposureTime = const i0.Value.absent(), - i0.Value fNumber = const i0.Value.absent(), + i0.Value fNumber = const i0.Value.absent(), i0.Value fileSize = const i0.Value.absent(), - i0.Value focalLength = const i0.Value.absent(), - i0.Value latitude = const i0.Value.absent(), - i0.Value longitude = const i0.Value.absent(), + i0.Value focalLength = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), i0.Value iso = const i0.Value.absent(), i0.Value make = const i0.Value.absent(), i0.Value model = const i0.Value.absent(), + i0.Value lens = const i0.Value.absent(), i0.Value orientation = const i0.Value.absent(), i0.Value timeZone = const i0.Value.absent(), i0.Value rating = const i0.Value.absent(), @@ -493,6 +507,7 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager< iso: iso, make: make, model: model, + lens: lens, orientation: orientation, timeZone: timeZone, rating: rating, @@ -622,9 +637,9 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity static const i0.VerificationMeta _fNumberMeta = const i0.VerificationMeta('fNumber'); @override - late final i0.GeneratedColumn fNumber = i0.GeneratedColumn( + late final i0.GeneratedColumn fNumber = i0.GeneratedColumn( 'f_number', aliasedName, true, - type: i0.DriftSqlType.int, requiredDuringInsert: false); + type: i0.DriftSqlType.double, requiredDuringInsert: false); static const i0.VerificationMeta _fileSizeMeta = const i0.VerificationMeta('fileSize'); @override @@ -634,21 +649,21 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity static const i0.VerificationMeta _focalLengthMeta = const i0.VerificationMeta('focalLength'); @override - late final i0.GeneratedColumn focalLength = i0.GeneratedColumn( - 'focal_length', aliasedName, true, - type: i0.DriftSqlType.int, requiredDuringInsert: false); + late final i0.GeneratedColumn focalLength = + i0.GeneratedColumn('focal_length', aliasedName, true, + type: i0.DriftSqlType.double, requiredDuringInsert: false); static const i0.VerificationMeta _latitudeMeta = const i0.VerificationMeta('latitude'); @override - late final i0.GeneratedColumn latitude = i0.GeneratedColumn( + late final i0.GeneratedColumn latitude = i0.GeneratedColumn( 'latitude', aliasedName, true, - type: i0.DriftSqlType.int, requiredDuringInsert: false); + type: i0.DriftSqlType.double, requiredDuringInsert: false); static const i0.VerificationMeta _longitudeMeta = const i0.VerificationMeta('longitude'); @override - late final i0.GeneratedColumn longitude = i0.GeneratedColumn( + late final i0.GeneratedColumn longitude = i0.GeneratedColumn( 'longitude', aliasedName, true, - type: i0.DriftSqlType.int, requiredDuringInsert: false); + type: i0.DriftSqlType.double, requiredDuringInsert: false); static const i0.VerificationMeta _isoMeta = const i0.VerificationMeta('iso'); @override late final i0.GeneratedColumn iso = i0.GeneratedColumn( @@ -666,6 +681,12 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity late final i0.GeneratedColumn model = i0.GeneratedColumn( 'model', aliasedName, true, type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _lensMeta = + const i0.VerificationMeta('lens'); + @override + late final i0.GeneratedColumn lens = i0.GeneratedColumn( + 'lens', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); static const i0.VerificationMeta _orientationMeta = const i0.VerificationMeta('orientation'); @override @@ -709,6 +730,7 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity iso, make, model, + lens, orientation, timeZone, rating, @@ -803,6 +825,10 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity context.handle( _modelMeta, model.isAcceptableOrUnknown(data['model']!, _modelMeta)); } + if (data.containsKey('lens')) { + context.handle( + _lensMeta, lens.isAcceptableOrUnknown(data['lens']!, _lensMeta)); + } if (data.containsKey('orientation')) { context.handle( _orientationMeta, @@ -853,21 +879,23 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity exposureTime: attachedDatabase.typeMapping.read( i0.DriftSqlType.string, data['${effectivePrefix}exposure_time']), fNumber: attachedDatabase.typeMapping - .read(i0.DriftSqlType.int, data['${effectivePrefix}f_number']), + .read(i0.DriftSqlType.double, data['${effectivePrefix}f_number']), fileSize: attachedDatabase.typeMapping .read(i0.DriftSqlType.int, data['${effectivePrefix}file_size']), focalLength: attachedDatabase.typeMapping - .read(i0.DriftSqlType.int, data['${effectivePrefix}focal_length']), + .read(i0.DriftSqlType.double, data['${effectivePrefix}focal_length']), latitude: attachedDatabase.typeMapping - .read(i0.DriftSqlType.int, data['${effectivePrefix}latitude']), + .read(i0.DriftSqlType.double, data['${effectivePrefix}latitude']), longitude: attachedDatabase.typeMapping - .read(i0.DriftSqlType.int, data['${effectivePrefix}longitude']), + .read(i0.DriftSqlType.double, data['${effectivePrefix}longitude']), iso: attachedDatabase.typeMapping .read(i0.DriftSqlType.int, data['${effectivePrefix}iso']), make: attachedDatabase.typeMapping .read(i0.DriftSqlType.string, data['${effectivePrefix}make']), model: attachedDatabase.typeMapping .read(i0.DriftSqlType.string, data['${effectivePrefix}model']), + lens: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}lens']), orientation: attachedDatabase.typeMapping .read(i0.DriftSqlType.string, data['${effectivePrefix}orientation']), timeZone: attachedDatabase.typeMapping @@ -901,14 +929,15 @@ class RemoteExifEntityData extends i0.DataClass final int? height; final int? width; final String? exposureTime; - final int? fNumber; + final double? fNumber; final int? fileSize; - final int? focalLength; - final int? latitude; - final int? longitude; + final double? focalLength; + final double? latitude; + final double? longitude; final int? iso; final String? make; final String? model; + final String? lens; final String? orientation; final String? timeZone; final int? rating; @@ -931,6 +960,7 @@ class RemoteExifEntityData extends i0.DataClass this.iso, this.make, this.model, + this.lens, this.orientation, this.timeZone, this.rating, @@ -964,19 +994,19 @@ class RemoteExifEntityData extends i0.DataClass map['exposure_time'] = i0.Variable(exposureTime); } if (!nullToAbsent || fNumber != null) { - map['f_number'] = i0.Variable(fNumber); + map['f_number'] = i0.Variable(fNumber); } if (!nullToAbsent || fileSize != null) { map['file_size'] = i0.Variable(fileSize); } if (!nullToAbsent || focalLength != null) { - map['focal_length'] = i0.Variable(focalLength); + map['focal_length'] = i0.Variable(focalLength); } if (!nullToAbsent || latitude != null) { - map['latitude'] = i0.Variable(latitude); + map['latitude'] = i0.Variable(latitude); } if (!nullToAbsent || longitude != null) { - map['longitude'] = i0.Variable(longitude); + map['longitude'] = i0.Variable(longitude); } if (!nullToAbsent || iso != null) { map['iso'] = i0.Variable(iso); @@ -987,6 +1017,9 @@ class RemoteExifEntityData extends i0.DataClass if (!nullToAbsent || model != null) { map['model'] = i0.Variable(model); } + if (!nullToAbsent || lens != null) { + map['lens'] = i0.Variable(lens); + } if (!nullToAbsent || orientation != null) { map['orientation'] = i0.Variable(orientation); } @@ -1016,14 +1049,15 @@ class RemoteExifEntityData extends i0.DataClass height: serializer.fromJson(json['height']), width: serializer.fromJson(json['width']), exposureTime: serializer.fromJson(json['exposureTime']), - fNumber: serializer.fromJson(json['fNumber']), + fNumber: serializer.fromJson(json['fNumber']), fileSize: serializer.fromJson(json['fileSize']), - focalLength: serializer.fromJson(json['focalLength']), - latitude: serializer.fromJson(json['latitude']), - longitude: serializer.fromJson(json['longitude']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), iso: serializer.fromJson(json['iso']), make: serializer.fromJson(json['make']), model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), orientation: serializer.fromJson(json['orientation']), timeZone: serializer.fromJson(json['timeZone']), rating: serializer.fromJson(json['rating']), @@ -1043,14 +1077,15 @@ class RemoteExifEntityData extends i0.DataClass 'height': serializer.toJson(height), 'width': serializer.toJson(width), 'exposureTime': serializer.toJson(exposureTime), - 'fNumber': serializer.toJson(fNumber), + 'fNumber': serializer.toJson(fNumber), 'fileSize': serializer.toJson(fileSize), - 'focalLength': serializer.toJson(focalLength), - 'latitude': serializer.toJson(latitude), - 'longitude': serializer.toJson(longitude), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), 'iso': serializer.toJson(iso), 'make': serializer.toJson(make), 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), 'orientation': serializer.toJson(orientation), 'timeZone': serializer.toJson(timeZone), 'rating': serializer.toJson(rating), @@ -1068,14 +1103,15 @@ class RemoteExifEntityData extends i0.DataClass i0.Value height = const i0.Value.absent(), i0.Value width = const i0.Value.absent(), i0.Value exposureTime = const i0.Value.absent(), - i0.Value fNumber = const i0.Value.absent(), + i0.Value fNumber = const i0.Value.absent(), i0.Value fileSize = const i0.Value.absent(), - i0.Value focalLength = const i0.Value.absent(), - i0.Value latitude = const i0.Value.absent(), - i0.Value longitude = const i0.Value.absent(), + i0.Value focalLength = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), i0.Value iso = const i0.Value.absent(), i0.Value make = const i0.Value.absent(), i0.Value model = const i0.Value.absent(), + i0.Value lens = const i0.Value.absent(), i0.Value orientation = const i0.Value.absent(), i0.Value timeZone = const i0.Value.absent(), i0.Value rating = const i0.Value.absent(), @@ -1101,6 +1137,7 @@ class RemoteExifEntityData extends i0.DataClass iso: iso.present ? iso.value : this.iso, make: make.present ? make.value : this.make, model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, orientation: orientation.present ? orientation.value : this.orientation, timeZone: timeZone.present ? timeZone.value : this.timeZone, rating: rating.present ? rating.value : this.rating, @@ -1132,6 +1169,7 @@ class RemoteExifEntityData extends i0.DataClass iso: data.iso.present ? data.iso.value : this.iso, make: data.make.present ? data.make.value : this.make, model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, orientation: data.orientation.present ? data.orientation.value : this.orientation, timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, @@ -1162,6 +1200,7 @@ class RemoteExifEntityData extends i0.DataClass ..write('iso: $iso, ') ..write('make: $make, ') ..write('model: $model, ') + ..write('lens: $lens, ') ..write('orientation: $orientation, ') ..write('timeZone: $timeZone, ') ..write('rating: $rating, ') @@ -1189,6 +1228,7 @@ class RemoteExifEntityData extends i0.DataClass iso, make, model, + lens, orientation, timeZone, rating, @@ -1215,6 +1255,7 @@ class RemoteExifEntityData extends i0.DataClass other.iso == this.iso && other.make == this.make && other.model == this.model && + other.lens == this.lens && other.orientation == this.orientation && other.timeZone == this.timeZone && other.rating == this.rating && @@ -1232,14 +1273,15 @@ class RemoteExifEntityCompanion final i0.Value height; final i0.Value width; final i0.Value exposureTime; - final i0.Value fNumber; + final i0.Value fNumber; final i0.Value fileSize; - final i0.Value focalLength; - final i0.Value latitude; - final i0.Value longitude; + final i0.Value focalLength; + final i0.Value latitude; + final i0.Value longitude; final i0.Value iso; final i0.Value make; final i0.Value model; + final i0.Value lens; final i0.Value orientation; final i0.Value timeZone; final i0.Value rating; @@ -1262,6 +1304,7 @@ class RemoteExifEntityCompanion this.iso = const i0.Value.absent(), this.make = const i0.Value.absent(), this.model = const i0.Value.absent(), + this.lens = const i0.Value.absent(), this.orientation = const i0.Value.absent(), this.timeZone = const i0.Value.absent(), this.rating = const i0.Value.absent(), @@ -1285,6 +1328,7 @@ class RemoteExifEntityCompanion this.iso = const i0.Value.absent(), this.make = const i0.Value.absent(), this.model = const i0.Value.absent(), + this.lens = const i0.Value.absent(), this.orientation = const i0.Value.absent(), this.timeZone = const i0.Value.absent(), this.rating = const i0.Value.absent(), @@ -1300,14 +1344,15 @@ class RemoteExifEntityCompanion i0.Expression? height, i0.Expression? width, i0.Expression? exposureTime, - i0.Expression? fNumber, + i0.Expression? fNumber, i0.Expression? fileSize, - i0.Expression? focalLength, - i0.Expression? latitude, - i0.Expression? longitude, + i0.Expression? focalLength, + i0.Expression? latitude, + i0.Expression? longitude, i0.Expression? iso, i0.Expression? make, i0.Expression? model, + i0.Expression? lens, i0.Expression? orientation, i0.Expression? timeZone, i0.Expression? rating, @@ -1331,6 +1376,7 @@ class RemoteExifEntityCompanion if (iso != null) 'iso': iso, if (make != null) 'make': make, if (model != null) 'model': model, + if (lens != null) 'lens': lens, if (orientation != null) 'orientation': orientation, if (timeZone != null) 'time_zone': timeZone, if (rating != null) 'rating': rating, @@ -1348,14 +1394,15 @@ class RemoteExifEntityCompanion i0.Value? height, i0.Value? width, i0.Value? exposureTime, - i0.Value? fNumber, + i0.Value? fNumber, i0.Value? fileSize, - i0.Value? focalLength, - i0.Value? latitude, - i0.Value? longitude, + i0.Value? focalLength, + i0.Value? latitude, + i0.Value? longitude, i0.Value? iso, i0.Value? make, i0.Value? model, + i0.Value? lens, i0.Value? orientation, i0.Value? timeZone, i0.Value? rating, @@ -1378,6 +1425,7 @@ class RemoteExifEntityCompanion iso: iso ?? this.iso, make: make ?? this.make, model: model ?? this.model, + lens: lens ?? this.lens, orientation: orientation ?? this.orientation, timeZone: timeZone ?? this.timeZone, rating: rating ?? this.rating, @@ -1416,19 +1464,19 @@ class RemoteExifEntityCompanion map['exposure_time'] = i0.Variable(exposureTime.value); } if (fNumber.present) { - map['f_number'] = i0.Variable(fNumber.value); + map['f_number'] = i0.Variable(fNumber.value); } if (fileSize.present) { map['file_size'] = i0.Variable(fileSize.value); } if (focalLength.present) { - map['focal_length'] = i0.Variable(focalLength.value); + map['focal_length'] = i0.Variable(focalLength.value); } if (latitude.present) { - map['latitude'] = i0.Variable(latitude.value); + map['latitude'] = i0.Variable(latitude.value); } if (longitude.present) { - map['longitude'] = i0.Variable(longitude.value); + map['longitude'] = i0.Variable(longitude.value); } if (iso.present) { map['iso'] = i0.Variable(iso.value); @@ -1439,6 +1487,9 @@ class RemoteExifEntityCompanion if (model.present) { map['model'] = i0.Variable(model.value); } + if (lens.present) { + map['lens'] = i0.Variable(lens.value); + } if (orientation.present) { map['orientation'] = i0.Variable(orientation.value); } @@ -1474,6 +1525,7 @@ class RemoteExifEntityCompanion ..write('iso: $iso, ') ..write('make: $make, ') ..write('model: $model, ') + ..write('lens: $lens, ') ..write('orientation: $orientation, ') ..write('timeZone: $timeZone, ') ..write('rating: $rating, ') diff --git a/mobile/lib/infrastructure/entities/local_album.entity.dart b/mobile/lib/infrastructure/entities/local_album.entity.dart index 9657173c3c..c796a12956 100644 --- a/mobile/lib/infrastructure/entities/local_album.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album.entity.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class LocalAlbumEntity extends Table with DriftDefaultsMixin { @@ -9,8 +9,7 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin { TextColumn get name => text()(); DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); IntColumn get backupSelection => intEnum()(); - BoolColumn get isIosSharedAlbum => - boolean().withDefault(const Constant(false))(); + BoolColumn get isIosSharedAlbum => boolean().withDefault(const Constant(false))(); // Used for mark & sweep BoolColumn get marker_ => boolean().nullable()(); diff --git a/mobile/lib/infrastructure/entities/local_album.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart index ff6226ba3f..06f65e25d8 100644 --- a/mobile/lib/infrastructure/entities/local_album.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart @@ -3,7 +3,7 @@ import 'package:drift/drift.dart' as i0; import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' as i1; -import 'package:immich_mobile/domain/models/local_album.model.dart' as i2; +import 'package:immich_mobile/domain/models/album/local_album.model.dart' as i2; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart' as i3; import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart index b64b9ec2fb..8de879a09d 100644 --- a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart @@ -6,11 +6,9 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class LocalAlbumAssetEntity extends Table with DriftDefaultsMixin { const LocalAlbumAssetEntity(); - TextColumn get assetId => - text().references(LocalAssetEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get assetId => text().references(LocalAssetEntity, #id, onDelete: KeyAction.cascade)(); - TextColumn get albumId => - text().references(LocalAlbumEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get albumId => text().references(LocalAlbumEntity, #id, onDelete: KeyAction.cascade)(); @override Set get primaryKey => {assetId, albumId}; diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index 39c3822b04..204d5d6a80 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -14,6 +14,8 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { // Only used during backup to mirror the favorite status of the asset in the server BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); + IntColumn get orientation => integer().withDefault(const Constant(0))(); + @override Set get primaryKey => {id}; } @@ -28,5 +30,9 @@ extension LocalAssetEntityDataDomainEx on LocalAssetEntityData { updatedAt: updatedAt, durationInSeconds: durationInSeconds, isFavorite: isFavorite, + height: height, + width: width, + remoteId: null, + orientation: orientation, ); } diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart index a3c79b2e2e..e9c5961aa5 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart @@ -20,6 +20,7 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder required String id, i0.Value checksum, i0.Value isFavorite, + i0.Value orientation, }); typedef $$LocalAssetEntityTableUpdateCompanionBuilder = i1.LocalAssetEntityCompanion Function({ @@ -33,6 +34,7 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder i0.Value id, i0.Value checksum, i0.Value isFavorite, + i0.Value orientation, }); class $$LocalAssetEntityTableFilterComposer @@ -76,6 +78,10 @@ class $$LocalAssetEntityTableFilterComposer i0.ColumnFilters get isFavorite => $composableBuilder( column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get orientation => $composableBuilder( + column: $table.orientation, + builder: (column) => i0.ColumnFilters(column)); } class $$LocalAssetEntityTableOrderingComposer @@ -120,6 +126,10 @@ class $$LocalAssetEntityTableOrderingComposer i0.ColumnOrderings get isFavorite => $composableBuilder( column: $table.isFavorite, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get orientation => $composableBuilder( + column: $table.orientation, + builder: (column) => i0.ColumnOrderings(column)); } class $$LocalAssetEntityTableAnnotationComposer @@ -160,6 +170,9 @@ class $$LocalAssetEntityTableAnnotationComposer i0.GeneratedColumn get isFavorite => $composableBuilder( column: $table.isFavorite, builder: (column) => column); + + i0.GeneratedColumn get orientation => $composableBuilder( + column: $table.orientation, builder: (column) => column); } class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< @@ -201,6 +214,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< i0.Value id = const i0.Value.absent(), i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), }) => i1.LocalAssetEntityCompanion( name: name, @@ -213,6 +227,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< id: id, checksum: checksum, isFavorite: isFavorite, + orientation: orientation, ), createCompanionCallback: ({ required String name, @@ -225,6 +240,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< required String id, i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), }) => i1.LocalAssetEntityCompanion.insert( name: name, @@ -237,6 +253,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< id: id, checksum: checksum, isFavorite: isFavorite, + orientation: orientation, ), withReferenceMapper: (p0) => p0 .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) @@ -337,6 +354,14 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity defaultConstraints: i0.GeneratedColumn.constraintIsAlways( 'CHECK ("is_favorite" IN (0, 1))'), defaultValue: const i4.Constant(false)); + static const i0.VerificationMeta _orientationMeta = + const i0.VerificationMeta('orientation'); + @override + late final i0.GeneratedColumn orientation = i0.GeneratedColumn( + 'orientation', aliasedName, false, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const i4.Constant(0)); @override List get $columns => [ name, @@ -348,7 +373,8 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity durationInSeconds, id, checksum, - isFavorite + isFavorite, + orientation ]; @override String get aliasedName => _alias ?? actualTableName; @@ -404,6 +430,12 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity isFavorite.isAcceptableOrUnknown( data['is_favorite']!, _isFavoriteMeta)); } + if (data.containsKey('orientation')) { + context.handle( + _orientationMeta, + orientation.isAcceptableOrUnknown( + data['orientation']!, _orientationMeta)); + } return context; } @@ -435,6 +467,8 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']), isFavorite: attachedDatabase.typeMapping .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + orientation: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}orientation'])!, ); } @@ -463,6 +497,7 @@ class LocalAssetEntityData extends i0.DataClass final String id; final String? checksum; final bool isFavorite; + final int orientation; const LocalAssetEntityData( {required this.name, required this.type, @@ -473,7 +508,8 @@ class LocalAssetEntityData extends i0.DataClass this.durationInSeconds, required this.id, this.checksum, - required this.isFavorite}); + required this.isFavorite, + required this.orientation}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -498,6 +534,7 @@ class LocalAssetEntityData extends i0.DataClass map['checksum'] = i0.Variable(checksum); } map['is_favorite'] = i0.Variable(isFavorite); + map['orientation'] = i0.Variable(orientation); return map; } @@ -516,6 +553,7 @@ class LocalAssetEntityData extends i0.DataClass id: serializer.fromJson(json['id']), checksum: serializer.fromJson(json['checksum']), isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), ); } @override @@ -533,6 +571,7 @@ class LocalAssetEntityData extends i0.DataClass 'id': serializer.toJson(id), 'checksum': serializer.toJson(checksum), 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), }; } @@ -546,7 +585,8 @@ class LocalAssetEntityData extends i0.DataClass i0.Value durationInSeconds = const i0.Value.absent(), String? id, i0.Value checksum = const i0.Value.absent(), - bool? isFavorite}) => + bool? isFavorite, + int? orientation}) => i1.LocalAssetEntityData( name: name ?? this.name, type: type ?? this.type, @@ -560,6 +600,7 @@ class LocalAssetEntityData extends i0.DataClass id: id ?? this.id, checksum: checksum.present ? checksum.value : this.checksum, isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, ); LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) { return LocalAssetEntityData( @@ -576,6 +617,8 @@ class LocalAssetEntityData extends i0.DataClass checksum: data.checksum.present ? data.checksum.value : this.checksum, isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + orientation: + data.orientation.present ? data.orientation.value : this.orientation, ); } @@ -591,14 +634,15 @@ class LocalAssetEntityData extends i0.DataClass ..write('durationInSeconds: $durationInSeconds, ') ..write('id: $id, ') ..write('checksum: $checksum, ') - ..write('isFavorite: $isFavorite') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') ..write(')')) .toString(); } @override int get hashCode => Object.hash(name, type, createdAt, updatedAt, width, - height, durationInSeconds, id, checksum, isFavorite); + height, durationInSeconds, id, checksum, isFavorite, orientation); @override bool operator ==(Object other) => identical(this, other) || @@ -612,7 +656,8 @@ class LocalAssetEntityData extends i0.DataClass other.durationInSeconds == this.durationInSeconds && other.id == this.id && other.checksum == this.checksum && - other.isFavorite == this.isFavorite); + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); } class LocalAssetEntityCompanion @@ -627,6 +672,7 @@ class LocalAssetEntityCompanion final i0.Value id; final i0.Value checksum; final i0.Value isFavorite; + final i0.Value orientation; const LocalAssetEntityCompanion({ this.name = const i0.Value.absent(), this.type = const i0.Value.absent(), @@ -638,6 +684,7 @@ class LocalAssetEntityCompanion this.id = const i0.Value.absent(), this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), + this.orientation = const i0.Value.absent(), }); LocalAssetEntityCompanion.insert({ required String name, @@ -650,6 +697,7 @@ class LocalAssetEntityCompanion required String id, this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), + this.orientation = const i0.Value.absent(), }) : name = i0.Value(name), type = i0.Value(type), id = i0.Value(id); @@ -664,6 +712,7 @@ class LocalAssetEntityCompanion i0.Expression? id, i0.Expression? checksum, i0.Expression? isFavorite, + i0.Expression? orientation, }) { return i0.RawValuesInsertable({ if (name != null) 'name': name, @@ -676,6 +725,7 @@ class LocalAssetEntityCompanion if (id != null) 'id': id, if (checksum != null) 'checksum': checksum, if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, }); } @@ -689,7 +739,8 @@ class LocalAssetEntityCompanion i0.Value? durationInSeconds, i0.Value? id, i0.Value? checksum, - i0.Value? isFavorite}) { + i0.Value? isFavorite, + i0.Value? orientation}) { return i1.LocalAssetEntityCompanion( name: name ?? this.name, type: type ?? this.type, @@ -701,6 +752,7 @@ class LocalAssetEntityCompanion id: id ?? this.id, checksum: checksum ?? this.checksum, isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, ); } @@ -738,6 +790,9 @@ class LocalAssetEntityCompanion if (isFavorite.present) { map['is_favorite'] = i0.Variable(isFavorite.value); } + if (orientation.present) { + map['orientation'] = i0.Variable(orientation.value); + } return map; } @@ -753,7 +808,8 @@ class LocalAssetEntityCompanion ..write('durationInSeconds: $durationInSeconds, ') ..write('id: $id, ') ..write('checksum: $checksum, ') - ..write('isFavorite: $isFavorite') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') ..write(')')) .toString(); } diff --git a/mobile/lib/infrastructure/entities/memory.entity.dart b/mobile/lib/infrastructure/entities/memory.entity.dart new file mode 100644 index 0000000000..63dcfef5cc --- /dev/null +++ b/mobile/lib/infrastructure/entities/memory.entity.dart @@ -0,0 +1,35 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class MemoryEntity extends Table with DriftDefaultsMixin { + const MemoryEntity(); + + TextColumn get id => text()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get deletedAt => dateTime().nullable()(); + + TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + IntColumn get type => intEnum()(); + + TextColumn get data => text()(); + + BoolColumn get isSaved => boolean().withDefault(const Constant(false))(); + + DateTimeColumn get memoryAt => dateTime()(); + + DateTimeColumn get seenAt => dateTime().nullable()(); + + DateTimeColumn get showAt => dateTime().nullable()(); + + DateTimeColumn get hideAt => dateTime().nullable()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/memory.entity.drift.dart b/mobile/lib/infrastructure/entities/memory.entity.drift.dart new file mode 100644 index 0000000000..cb88651ba4 --- /dev/null +++ b/mobile/lib/infrastructure/entities/memory.entity.drift.dart @@ -0,0 +1,970 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/memory.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/memory.entity.dart' as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; + +typedef $$MemoryEntityTableCreateCompanionBuilder = i1.MemoryEntityCompanion + Function({ + required String id, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value deletedAt, + required String ownerId, + required i2.MemoryTypeEnum type, + required String data, + i0.Value isSaved, + required DateTime memoryAt, + i0.Value seenAt, + i0.Value showAt, + i0.Value hideAt, +}); +typedef $$MemoryEntityTableUpdateCompanionBuilder = i1.MemoryEntityCompanion + Function({ + i0.Value id, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value deletedAt, + i0.Value ownerId, + i0.Value type, + i0.Value data, + i0.Value isSaved, + i0.Value memoryAt, + i0.Value seenAt, + i0.Value showAt, + i0.Value hideAt, +}); + +final class $$MemoryEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, i1.$MemoryEntityTable, i1.MemoryEntityData> { + $$MemoryEntityTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('memory_entity') + .ownerId, + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i5.$$UserEntityTableProcessedTableManager get ownerId { + final $_column = $_itemColumn('owner_id')!; + + final manager = i5 + .$$UserEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_ownerIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$MemoryEntityTableFilterComposer + extends i0.Composer { + $$MemoryEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters + get type => $composableBuilder( + column: $table.type, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get data => $composableBuilder( + column: $table.data, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isSaved => $composableBuilder( + column: $table.isSaved, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get memoryAt => $composableBuilder( + column: $table.memoryAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get seenAt => $composableBuilder( + column: $table.seenAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get showAt => $composableBuilder( + column: $table.showAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get hideAt => $composableBuilder( + column: $table.hideAt, builder: (column) => i0.ColumnFilters(column)); + + i5.$$UserEntityTableFilterComposer get ownerId { + final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MemoryEntityTableOrderingComposer + extends i0.Composer { + $$MemoryEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get data => $composableBuilder( + column: $table.data, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isSaved => $composableBuilder( + column: $table.isSaved, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get memoryAt => $composableBuilder( + column: $table.memoryAt, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get seenAt => $composableBuilder( + column: $table.seenAt, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get showAt => $composableBuilder( + column: $table.showAt, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get hideAt => $composableBuilder( + column: $table.hideAt, builder: (column) => i0.ColumnOrderings(column)); + + i5.$$UserEntityTableOrderingComposer get ownerId { + final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MemoryEntityTableAnnotationComposer + extends i0.Composer { + $$MemoryEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + i0.GeneratedColumn get data => + $composableBuilder(column: $table.data, builder: (column) => column); + + i0.GeneratedColumn get isSaved => + $composableBuilder(column: $table.isSaved, builder: (column) => column); + + i0.GeneratedColumn get memoryAt => + $composableBuilder(column: $table.memoryAt, builder: (column) => column); + + i0.GeneratedColumn get seenAt => + $composableBuilder(column: $table.seenAt, builder: (column) => column); + + i0.GeneratedColumn get showAt => + $composableBuilder(column: $table.showAt, builder: (column) => column); + + i0.GeneratedColumn get hideAt => + $composableBuilder(column: $table.hideAt, builder: (column) => column); + + i5.$$UserEntityTableAnnotationComposer get ownerId { + final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MemoryEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$MemoryEntityTable, + i1.MemoryEntityData, + i1.$$MemoryEntityTableFilterComposer, + i1.$$MemoryEntityTableOrderingComposer, + i1.$$MemoryEntityTableAnnotationComposer, + $$MemoryEntityTableCreateCompanionBuilder, + $$MemoryEntityTableUpdateCompanionBuilder, + (i1.MemoryEntityData, i1.$$MemoryEntityTableReferences), + i1.MemoryEntityData, + i0.PrefetchHooks Function({bool ownerId})> { + $$MemoryEntityTableTableManager( + i0.GeneratedDatabase db, i1.$MemoryEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$MemoryEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$MemoryEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$MemoryEntityTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + i0.Value ownerId = const i0.Value.absent(), + i0.Value type = const i0.Value.absent(), + i0.Value data = const i0.Value.absent(), + i0.Value isSaved = const i0.Value.absent(), + i0.Value memoryAt = const i0.Value.absent(), + i0.Value seenAt = const i0.Value.absent(), + i0.Value showAt = const i0.Value.absent(), + i0.Value hideAt = const i0.Value.absent(), + }) => + i1.MemoryEntityCompanion( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + ownerId: ownerId, + type: type, + data: data, + isSaved: isSaved, + memoryAt: memoryAt, + seenAt: seenAt, + showAt: showAt, + hideAt: hideAt, + ), + createCompanionCallback: ({ + required String id, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + required String ownerId, + required i2.MemoryTypeEnum type, + required String data, + i0.Value isSaved = const i0.Value.absent(), + required DateTime memoryAt, + i0.Value seenAt = const i0.Value.absent(), + i0.Value showAt = const i0.Value.absent(), + i0.Value hideAt = const i0.Value.absent(), + }) => + i1.MemoryEntityCompanion.insert( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + ownerId: ownerId, + type: type, + data: data, + isSaved: isSaved, + memoryAt: memoryAt, + seenAt: seenAt, + showAt: showAt, + hideAt: hideAt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$MemoryEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({ownerId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (ownerId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.ownerId, + referencedTable: + i1.$$MemoryEntityTableReferences._ownerIdTable(db), + referencedColumn: + i1.$$MemoryEntityTableReferences._ownerIdTable(db).id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$MemoryEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$MemoryEntityTable, + i1.MemoryEntityData, + i1.$$MemoryEntityTableFilterComposer, + i1.$$MemoryEntityTableOrderingComposer, + i1.$$MemoryEntityTableAnnotationComposer, + $$MemoryEntityTableCreateCompanionBuilder, + $$MemoryEntityTableUpdateCompanionBuilder, + (i1.MemoryEntityData, i1.$$MemoryEntityTableReferences), + i1.MemoryEntityData, + i0.PrefetchHooks Function({bool ownerId})>; + +class $MemoryEntityTable extends i3.MemoryEntity + with i0.TableInfo<$MemoryEntityTable, i1.MemoryEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $MemoryEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _deletedAtMeta = + const i0.VerificationMeta('deletedAt'); + @override + late final i0.GeneratedColumn deletedAt = + i0.GeneratedColumn('deleted_at', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _ownerIdMeta = + const i0.VerificationMeta('ownerId'); + @override + late final i0.GeneratedColumn ownerId = i0.GeneratedColumn( + 'owner_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + @override + late final i0.GeneratedColumnWithTypeConverter type = + i0.GeneratedColumn('type', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$MemoryEntityTable.$convertertype); + static const i0.VerificationMeta _dataMeta = + const i0.VerificationMeta('data'); + @override + late final i0.GeneratedColumn data = i0.GeneratedColumn( + 'data', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _isSavedMeta = + const i0.VerificationMeta('isSaved'); + @override + late final i0.GeneratedColumn isSaved = i0.GeneratedColumn( + 'is_saved', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + i0.GeneratedColumn.constraintIsAlways('CHECK ("is_saved" IN (0, 1))'), + defaultValue: const i4.Constant(false)); + static const i0.VerificationMeta _memoryAtMeta = + const i0.VerificationMeta('memoryAt'); + @override + late final i0.GeneratedColumn memoryAt = + i0.GeneratedColumn('memory_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: true); + static const i0.VerificationMeta _seenAtMeta = + const i0.VerificationMeta('seenAt'); + @override + late final i0.GeneratedColumn seenAt = i0.GeneratedColumn( + 'seen_at', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _showAtMeta = + const i0.VerificationMeta('showAt'); + @override + late final i0.GeneratedColumn showAt = i0.GeneratedColumn( + 'show_at', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _hideAtMeta = + const i0.VerificationMeta('hideAt'); + @override + late final i0.GeneratedColumn hideAt = i0.GeneratedColumn( + 'hide_at', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('deleted_at')) { + context.handle(_deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta)); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('data')) { + context.handle( + _dataMeta, this.data.isAcceptableOrUnknown(data['data']!, _dataMeta)); + } else if (isInserting) { + context.missing(_dataMeta); + } + if (data.containsKey('is_saved')) { + context.handle(_isSavedMeta, + isSaved.isAcceptableOrUnknown(data['is_saved']!, _isSavedMeta)); + } + if (data.containsKey('memory_at')) { + context.handle(_memoryAtMeta, + memoryAt.isAcceptableOrUnknown(data['memory_at']!, _memoryAtMeta)); + } else if (isInserting) { + context.missing(_memoryAtMeta); + } + if (data.containsKey('seen_at')) { + context.handle(_seenAtMeta, + seenAt.isAcceptableOrUnknown(data['seen_at']!, _seenAtMeta)); + } + if (data.containsKey('show_at')) { + context.handle(_showAtMeta, + showAt.isAcceptableOrUnknown(data['show_at']!, _showAtMeta)); + } + if (data.containsKey('hide_at')) { + context.handle(_hideAtMeta, + hideAt.isAcceptableOrUnknown(data['hide_at']!, _hideAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.MemoryEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + deletedAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + ownerId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + type: i1.$MemoryEntityTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!), + data: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}data'])!, + isSaved: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_saved'])!, + memoryAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}memory_at'])!, + seenAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}seen_at']), + showAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}show_at']), + hideAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}hide_at']), + ); + } + + @override + $MemoryEntityTable createAlias(String alias) { + return $MemoryEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $convertertype = + const i0.EnumIndexConverter(i2.MemoryTypeEnum.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final i2.MemoryTypeEnum type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = i0.Variable(deletedAt); + } + map['owner_id'] = i0.Variable(ownerId); + { + map['type'] = + i0.Variable(i1.$MemoryEntityTable.$convertertype.toSql(type)); + } + map['data'] = i0.Variable(data); + map['is_saved'] = i0.Variable(isSaved); + map['memory_at'] = i0.Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = i0.Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = i0.Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = i0.Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: i1.$MemoryEntityTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer + .toJson(i1.$MemoryEntityTable.$convertertype.toJson(type)), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + i1.MemoryEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + i0.Value deletedAt = const i0.Value.absent(), + String? ownerId, + i2.MemoryTypeEnum? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + i0.Value seenAt = const i0.Value.absent(), + i0.Value showAt = const i0.Value.absent(), + i0.Value hideAt = const i0.Value.absent()}) => + i1.MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(i1.MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, deletedAt, ownerId, + type, data, isSaved, memoryAt, seenAt, showAt, hideAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value deletedAt; + final i0.Value ownerId; + final i0.Value type; + final i0.Value data; + final i0.Value isSaved; + final i0.Value memoryAt; + final i0.Value seenAt; + final i0.Value showAt; + final i0.Value hideAt; + const MemoryEntityCompanion({ + this.id = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), + this.ownerId = const i0.Value.absent(), + this.type = const i0.Value.absent(), + this.data = const i0.Value.absent(), + this.isSaved = const i0.Value.absent(), + this.memoryAt = const i0.Value.absent(), + this.seenAt = const i0.Value.absent(), + this.showAt = const i0.Value.absent(), + this.hideAt = const i0.Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), + required String ownerId, + required i2.MemoryTypeEnum type, + required String data, + this.isSaved = const i0.Value.absent(), + required DateTime memoryAt, + this.seenAt = const i0.Value.absent(), + this.showAt = const i0.Value.absent(), + this.hideAt = const i0.Value.absent(), + }) : id = i0.Value(id), + ownerId = i0.Value(ownerId), + type = i0.Value(type), + data = i0.Value(data), + memoryAt = i0.Value(memoryAt); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? deletedAt, + i0.Expression? ownerId, + i0.Expression? type, + i0.Expression? data, + i0.Expression? isSaved, + i0.Expression? memoryAt, + i0.Expression? seenAt, + i0.Expression? showAt, + i0.Expression? hideAt, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + i1.MemoryEntityCompanion copyWith( + {i0.Value? id, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? deletedAt, + i0.Value? ownerId, + i0.Value? type, + i0.Value? data, + i0.Value? isSaved, + i0.Value? memoryAt, + i0.Value? seenAt, + i0.Value? showAt, + i0.Value? hideAt}) { + return i1.MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = i0.Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = i0.Variable(ownerId.value); + } + if (type.present) { + map['type'] = i0.Variable( + i1.$MemoryEntityTable.$convertertype.toSql(type.value)); + } + if (data.present) { + map['data'] = i0.Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = i0.Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = i0.Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = i0.Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = i0.Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = i0.Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/memory_asset.entity.dart b/mobile/lib/infrastructure/entities/memory_asset.entity.dart new file mode 100644 index 0000000000..5053afdfb3 --- /dev/null +++ b/mobile/lib/infrastructure/entities/memory_asset.entity.dart @@ -0,0 +1,15 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class MemoryAssetEntity extends Table with DriftDefaultsMixin { + const MemoryAssetEntity(); + + TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get memoryId => text().references(MemoryEntity, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {assetId, memoryId}; +} diff --git a/mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart new file mode 100644 index 0000000000..9253e8bc05 --- /dev/null +++ b/mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart @@ -0,0 +1,550 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart' + as i2; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i3; +import 'package:drift/internal/modular.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' + as i5; + +typedef $$MemoryAssetEntityTableCreateCompanionBuilder + = i1.MemoryAssetEntityCompanion Function({ + required String assetId, + required String memoryId, +}); +typedef $$MemoryAssetEntityTableUpdateCompanionBuilder + = i1.MemoryAssetEntityCompanion Function({ + i0.Value assetId, + i0.Value memoryId, +}); + +final class $$MemoryAssetEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$MemoryAssetEntityTable, + i1.MemoryAssetEntityData> { + $$MemoryAssetEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet('memory_asset_entity') + .assetId, + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .id)); + + i3.$$RemoteAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i3 + .$$RemoteAssetEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('remote_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i5.$MemoryEntityTable _memoryIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('memory_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet('memory_asset_entity') + .memoryId, + i4.ReadDatabaseContainer(db) + .resultSet('memory_entity') + .id)); + + i5.$$MemoryEntityTableProcessedTableManager get memoryId { + final $_column = $_itemColumn('memory_id')!; + + final manager = i5 + .$$MemoryEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('memory_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_memoryIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$MemoryAssetEntityTableFilterComposer + extends i0.Composer { + $$MemoryAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$RemoteAssetEntityTableFilterComposer get assetId { + final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$MemoryEntityTableFilterComposer get memoryId { + final i5.$$MemoryEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.memoryId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('memory_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$MemoryEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('memory_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MemoryAssetEntityTableOrderingComposer + extends i0.Composer { + $$MemoryAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$RemoteAssetEntityTableOrderingComposer get assetId { + final i3.$$RemoteAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$MemoryEntityTableOrderingComposer get memoryId { + final i5.$$MemoryEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.memoryId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('memory_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$MemoryEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('memory_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MemoryAssetEntityTableAnnotationComposer + extends i0.Composer { + $$MemoryAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$RemoteAssetEntityTableAnnotationComposer get assetId { + final i3.$$RemoteAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$MemoryEntityTableAnnotationComposer get memoryId { + final i5.$$MemoryEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.memoryId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('memory_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$MemoryEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('memory_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MemoryAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$MemoryAssetEntityTable, + i1.MemoryAssetEntityData, + i1.$$MemoryAssetEntityTableFilterComposer, + i1.$$MemoryAssetEntityTableOrderingComposer, + i1.$$MemoryAssetEntityTableAnnotationComposer, + $$MemoryAssetEntityTableCreateCompanionBuilder, + $$MemoryAssetEntityTableUpdateCompanionBuilder, + (i1.MemoryAssetEntityData, i1.$$MemoryAssetEntityTableReferences), + i1.MemoryAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool memoryId})> { + $$MemoryAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$MemoryAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$MemoryAssetEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$MemoryAssetEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$MemoryAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value assetId = const i0.Value.absent(), + i0.Value memoryId = const i0.Value.absent(), + }) => + i1.MemoryAssetEntityCompanion( + assetId: assetId, + memoryId: memoryId, + ), + createCompanionCallback: ({ + required String assetId, + required String memoryId, + }) => + i1.MemoryAssetEntityCompanion.insert( + assetId: assetId, + memoryId: memoryId, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$MemoryAssetEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({assetId = false, memoryId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (assetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: + i1.$$MemoryAssetEntityTableReferences._assetIdTable(db), + referencedColumn: i1.$$MemoryAssetEntityTableReferences + ._assetIdTable(db) + .id, + ) as T; + } + if (memoryId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.memoryId, + referencedTable: i1.$$MemoryAssetEntityTableReferences + ._memoryIdTable(db), + referencedColumn: i1.$$MemoryAssetEntityTableReferences + ._memoryIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$MemoryAssetEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$MemoryAssetEntityTable, + i1.MemoryAssetEntityData, + i1.$$MemoryAssetEntityTableFilterComposer, + i1.$$MemoryAssetEntityTableOrderingComposer, + i1.$$MemoryAssetEntityTableAnnotationComposer, + $$MemoryAssetEntityTableCreateCompanionBuilder, + $$MemoryAssetEntityTableUpdateCompanionBuilder, + (i1.MemoryAssetEntityData, i1.$$MemoryAssetEntityTableReferences), + i1.MemoryAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool memoryId})>; + +class $MemoryAssetEntityTable extends i2.MemoryAssetEntity + with i0.TableInfo<$MemoryAssetEntityTable, i1.MemoryAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $MemoryAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _assetIdMeta = + const i0.VerificationMeta('assetId'); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _memoryIdMeta = + const i0.VerificationMeta('memoryId'); + @override + late final i0.GeneratedColumn memoryId = i0.GeneratedColumn( + 'memory_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('asset_id')) { + context.handle(_assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + if (data.containsKey('memory_id')) { + context.handle(_memoryIdMeta, + memoryId.isAcceptableOrUnknown(data['memory_id']!, _memoryIdMeta)); + } else if (isInserting) { + context.missing(_memoryIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {assetId, memoryId}; + @override + i1.MemoryAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + memoryId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}memory_id'])!, + ); + } + + @override + $MemoryAssetEntityTable createAlias(String alias) { + return $MemoryAssetEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = i0.Variable(assetId); + map['memory_id'] = i0.Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + i1.MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + i1.MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(i1.MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value assetId; + final i0.Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const i0.Value.absent(), + this.memoryId = const i0.Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = i0.Value(assetId), + memoryId = i0.Value(memoryId); + static i0.Insertable custom({ + i0.Expression? assetId, + i0.Expression? memoryId, + }) { + return i0.RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + i1.MemoryAssetEntityCompanion copyWith( + {i0.Value? assetId, i0.Value? memoryId}) { + return i1.MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = i0.Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift b/mobile/lib/infrastructure/entities/merged_asset.drift index 51f731f0ff..9778ba723b 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift +++ b/mobile/lib/infrastructure/entities/merged_asset.drift @@ -1,81 +1,109 @@ import 'remote_asset.entity.dart'; +import 'stack.entity.dart'; import 'local_asset.entity.dart'; +import 'local_album.entity.dart'; +import 'local_album_asset.entity.dart'; -mergedAsset: SELECT * FROM -( - SELECT - rae.id as remote_id, - lae.id as local_id, - rae.name, - rae."type", - rae.created_at, - rae.updated_at, - rae.width, - rae.height, - rae.duration_in_seconds, - rae.is_favorite, - rae.thumb_hash, - rae.checksum, - rae.owner_id - FROM - remote_asset_entity rae - LEFT JOIN - local_asset_entity lae ON rae.checksum = lae.checksum - WHERE - rae.visibility = 0 AND rae.owner_id in ? - UNION ALL - SELECT - NULL as remote_id, - lae.id as local_id, - lae.name, - lae."type", - lae.created_at, - lae.updated_at, - lae.width, - lae.height, - lae.duration_in_seconds, - lae.is_favorite, - NULL as thumb_hash, - lae.checksum, - NULL as owner_id - FROM - local_asset_entity lae - LEFT JOIN - remote_asset_entity rae ON rae.checksum = lae.checksum - WHERE - rae.id IS NULL +mergedAsset: +SELECT + rae.id as remote_id, + (SELECT lae.id FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1) as local_id, + rae.name, + rae."type", + rae.created_at as created_at, + rae.updated_at, + rae.width, + rae.height, + rae.duration_in_seconds, + rae.is_favorite, + rae.thumb_hash, + rae.checksum, + rae.owner_id, + rae.live_photo_video_id, + 0 as orientation, + rae.stack_id +FROM + remote_asset_entity rae +LEFT JOIN + stack_entity se ON rae.stack_id = se.id +WHERE + rae.deleted_at IS NULL + AND rae.visibility = 0 -- timeline visibility + AND rae.owner_id in ? + AND ( + rae.stack_id IS NULL + OR rae.id = se.primary_asset_id + ) + +UNION ALL + +SELECT + NULL as remote_id, + lae.id as local_id, + lae.name, + lae."type", + lae.created_at as created_at, + lae.updated_at, + lae.width, + lae.height, + lae.duration_in_seconds, + lae.is_favorite, + NULL as thumb_hash, + lae.checksum, + NULL as owner_id, + NULL as live_photo_video_id, + lae.orientation, + NULL as stack_id +FROM + local_asset_entity lae +WHERE NOT EXISTS ( + SELECT 1 FROM remote_asset_entity rae WHERE rae.checksum = lae.checksum +) +AND EXISTS ( + SELECT 1 FROM local_album_asset_entity laa + INNER JOIN local_album_entity la on laa.album_id = la.id + WHERE laa.asset_id = lae.id AND la.backup_selection = 0 -- selected ) ORDER BY created_at DESC LIMIT $limit; -mergedBucket(:group_by AS INTEGER): -SELECT +mergedBucket(:group_by AS INTEGER): +SELECT COUNT(*) as asset_count, CASE - WHEN :group_by = 0 THEN STRFTIME('%Y-%m-%d', created_at) -- day - WHEN :group_by = 1 THEN STRFTIME('%Y-%m', created_at) -- month + WHEN :group_by = 0 THEN STRFTIME('%Y-%m-%d', created_at, 'localtime') -- day + WHEN :group_by = 1 THEN STRFTIME('%Y-%m', created_at, 'localtime') -- month END AS bucket_date FROM ( SELECT - rae.name, rae.created_at FROM remote_asset_entity rae LEFT JOIN - local_asset_entity lae ON rae.checksum = lae.checksum + stack_entity se ON rae.stack_id = se.id WHERE - rae.visibility = 0 AND rae.owner_id in ? + rae.deleted_at IS NULL + AND rae.visibility = 0 -- timeline visibility + AND rae.owner_id in ? + AND ( + rae.stack_id IS NULL + OR rae.id = se.primary_asset_id + ) UNION ALL SELECT - lae.name, lae.created_at FROM local_asset_entity lae LEFT JOIN remote_asset_entity rae ON rae.checksum = lae.checksum + LEFT JOIN + local_album_asset_entity laa ON laa.asset_id = lae.id + LEFT JOIN + local_album_entity la ON la.id = laa.album_id WHERE rae.id IS NULL + AND la.backup_selection = 0 -- selected ) GROUP BY bucket_date ORDER BY bucket_date DESC; diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift.dart b/mobile/lib/infrastructure/entities/merged_asset.drift.dart index be9d8b521e..7f56b25d4e 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift.dart +++ b/mobile/lib/infrastructure/entities/merged_asset.drift.dart @@ -3,22 +3,29 @@ import 'package:drift/drift.dart' as i0; import 'package:drift/internal/modular.dart' as i1; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2; -import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' - as i3; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i3; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart' + as i5; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' + as i6; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i7; class MergedAssetDrift extends i1.ModularAccessor { MergedAssetDrift(i0.GeneratedDatabase db) : super(db); i0.Selectable mergedAsset(List var1, - {required i0.Limit limit}) { + {required MergedAsset$limit limit}) { var $arrayStartIndex = 1; final expandedvar1 = $expandVar($arrayStartIndex, var1.length); $arrayStartIndex += var1.length; - final generatedlimit = $write(limit, startIndex: $arrayStartIndex); + final generatedlimit = $write(limit(alias(this.localAssetEntity, 'lae')), + startIndex: $arrayStartIndex); $arrayStartIndex += generatedlimit.amountOfVariables; return customSelect( - 'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}', + 'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) ORDER BY created_at DESC ${generatedlimit.sql}', variables: [ for (var $ in var1) i0.Variable($), ...generatedlimit.introducedVariables @@ -26,12 +33,15 @@ class MergedAssetDrift extends i1.ModularAccessor { readsFrom: { remoteAssetEntity, localAssetEntity, + stackEntity, + localAlbumAssetEntity, + localAlbumEntity, ...generatedlimit.watchedTables, }).map((i0.QueryRow row) => MergedAssetResult( remoteId: row.readNullable('remote_id'), localId: row.readNullable('local_id'), name: row.read('name'), - type: i3.$RemoteAssetEntityTable.$convertertype + type: i4.$RemoteAssetEntityTable.$convertertype .fromSql(row.read('type')), createdAt: row.read('created_at'), updatedAt: row.read('updated_at'), @@ -42,6 +52,9 @@ class MergedAssetDrift extends i1.ModularAccessor { thumbHash: row.readNullable('thumb_hash'), checksum: row.readNullable('checksum'), ownerId: row.readNullable('owner_id'), + livePhotoVideoId: row.readNullable('live_photo_video_id'), + orientation: row.read('orientation'), + stackId: row.readNullable('stack_id'), )); } @@ -51,26 +64,39 @@ class MergedAssetDrift extends i1.ModularAccessor { final expandedvar2 = $expandVar($arrayStartIndex, var2.length); $arrayStartIndex += var2.length; return customSelect( - 'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at) WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at) END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC', + 'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at, \'localtime\') END AS bucket_date FROM (SELECT rae.created_at FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT lae.created_at FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum LEFT JOIN local_album_asset_entity AS laa ON laa.asset_id = lae.id LEFT JOIN local_album_entity AS la ON la.id = laa.album_id WHERE rae.id IS NULL AND la.backup_selection = 0) GROUP BY bucket_date ORDER BY bucket_date DESC', variables: [ i0.Variable(groupBy), for (var $ in var2) i0.Variable($) ], readsFrom: { remoteAssetEntity, + stackEntity, localAssetEntity, + localAlbumAssetEntity, + localAlbumEntity, }).map((i0.QueryRow row) => MergedBucketResult( assetCount: row.read('asset_count'), bucketDate: row.read('bucket_date'), )); } - i3.$RemoteAssetEntityTable get remoteAssetEntity => + i4.$RemoteAssetEntityTable get remoteAssetEntity => i1.ReadDatabaseContainer(attachedDatabase) - .resultSet('remote_asset_entity'); - i4.$LocalAssetEntityTable get localAssetEntity => + .resultSet('remote_asset_entity'); + i5.$StackEntityTable get stackEntity => i1.ReadDatabaseContainer(attachedDatabase) - .resultSet('local_asset_entity'); + .resultSet('stack_entity'); + i3.$LocalAssetEntityTable get localAssetEntity => + i1.ReadDatabaseContainer(attachedDatabase) + .resultSet('local_asset_entity'); + i6.$LocalAlbumAssetEntityTable get localAlbumAssetEntity => + i1.ReadDatabaseContainer(attachedDatabase) + .resultSet( + 'local_album_asset_entity'); + i7.$LocalAlbumEntityTable get localAlbumEntity => + i1.ReadDatabaseContainer(attachedDatabase) + .resultSet('local_album_entity'); } class MergedAssetResult { @@ -87,6 +113,9 @@ class MergedAssetResult { final String? thumbHash; final String? checksum; final String? ownerId; + final String? livePhotoVideoId; + final int orientation; + final String? stackId; MergedAssetResult({ this.remoteId, this.localId, @@ -101,9 +130,14 @@ class MergedAssetResult { this.thumbHash, this.checksum, this.ownerId, + this.livePhotoVideoId, + required this.orientation, + this.stackId, }); } +typedef MergedAsset$limit = i0.Limit Function(i3.$LocalAssetEntityTable lae); + class MergedBucketResult { final int assetCount; final String bucketDate; diff --git a/mobile/lib/infrastructure/entities/partner.entity.dart b/mobile/lib/infrastructure/entities/partner.entity.dart index 8b51d93e6f..dbc675ee99 100644 --- a/mobile/lib/infrastructure/entities/partner.entity.dart +++ b/mobile/lib/infrastructure/entities/partner.entity.dart @@ -5,11 +5,9 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class PartnerEntity extends Table with DriftDefaultsMixin { const PartnerEntity(); - TextColumn get sharedById => - text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get sharedById => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); - TextColumn get sharedWithId => - text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get sharedWithId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); BoolColumn get inTimeline => boolean().withDefault(const Constant(false))(); diff --git a/mobile/lib/infrastructure/entities/person.entity.dart b/mobile/lib/infrastructure/entities/person.entity.dart new file mode 100644 index 0000000000..f0878e00f8 --- /dev/null +++ b/mobile/lib/infrastructure/entities/person.entity.dart @@ -0,0 +1,30 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class PersonEntity extends Table with DriftDefaultsMixin { + const PersonEntity(); + + TextColumn get id => text()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get name => text()(); + + TextColumn get faceAssetId => text().nullable()(); + + BoolColumn get isFavorite => boolean()(); + + BoolColumn get isHidden => boolean()(); + + TextColumn get color => text().nullable()(); + + DateTimeColumn get birthDate => dateTime().nullable()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/person.entity.drift.dart b/mobile/lib/infrastructure/entities/person.entity.drift.dart new file mode 100644 index 0000000000..70639adc2f --- /dev/null +++ b/mobile/lib/infrastructure/entities/person.entity.drift.dart @@ -0,0 +1,875 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/person.entity.dart' as i2; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i4; +import 'package:drift/internal/modular.dart' as i5; + +typedef $$PersonEntityTableCreateCompanionBuilder = i1.PersonEntityCompanion + Function({ + required String id, + i0.Value createdAt, + i0.Value updatedAt, + required String ownerId, + required String name, + i0.Value faceAssetId, + required bool isFavorite, + required bool isHidden, + i0.Value color, + i0.Value birthDate, +}); +typedef $$PersonEntityTableUpdateCompanionBuilder = i1.PersonEntityCompanion + Function({ + i0.Value id, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value ownerId, + i0.Value name, + i0.Value faceAssetId, + i0.Value isFavorite, + i0.Value isHidden, + i0.Value color, + i0.Value birthDate, +}); + +final class $$PersonEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, i1.$PersonEntityTable, i1.PersonEntityData> { + $$PersonEntityTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static i4.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i5.ReadDatabaseContainer(db) + .resultSet('person_entity') + .ownerId, + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i4.$$UserEntityTableProcessedTableManager get ownerId { + final $_column = $_itemColumn('owner_id')!; + + final manager = i4 + .$$UserEntityTableTableManager( + $_db, + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_ownerIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$PersonEntityTableFilterComposer + extends i0.Composer { + $$PersonEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get faceAssetId => $composableBuilder( + column: $table.faceAssetId, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isHidden => $composableBuilder( + column: $table.isHidden, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get color => $composableBuilder( + column: $table.color, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get birthDate => $composableBuilder( + column: $table.birthDate, builder: (column) => i0.ColumnFilters(column)); + + i4.$$UserEntityTableFilterComposer get ownerId { + final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$UserEntityTableFilterComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$PersonEntityTableOrderingComposer + extends i0.Composer { + $$PersonEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get faceAssetId => $composableBuilder( + column: $table.faceAssetId, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isFavorite => $composableBuilder( + column: $table.isFavorite, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isHidden => $composableBuilder( + column: $table.isHidden, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get color => $composableBuilder( + column: $table.color, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get birthDate => $composableBuilder( + column: $table.birthDate, + builder: (column) => i0.ColumnOrderings(column)); + + i4.$$UserEntityTableOrderingComposer get ownerId { + final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$PersonEntityTableAnnotationComposer + extends i0.Composer { + $$PersonEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumn get faceAssetId => $composableBuilder( + column: $table.faceAssetId, builder: (column) => column); + + i0.GeneratedColumn get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => column); + + i0.GeneratedColumn get isHidden => + $composableBuilder(column: $table.isHidden, builder: (column) => column); + + i0.GeneratedColumn get color => + $composableBuilder(column: $table.color, builder: (column) => column); + + i0.GeneratedColumn get birthDate => + $composableBuilder(column: $table.birthDate, builder: (column) => column); + + i4.$$UserEntityTableAnnotationComposer get ownerId { + final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$PersonEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$PersonEntityTable, + i1.PersonEntityData, + i1.$$PersonEntityTableFilterComposer, + i1.$$PersonEntityTableOrderingComposer, + i1.$$PersonEntityTableAnnotationComposer, + $$PersonEntityTableCreateCompanionBuilder, + $$PersonEntityTableUpdateCompanionBuilder, + (i1.PersonEntityData, i1.$$PersonEntityTableReferences), + i1.PersonEntityData, + i0.PrefetchHooks Function({bool ownerId})> { + $$PersonEntityTableTableManager( + i0.GeneratedDatabase db, i1.$PersonEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$PersonEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$PersonEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$PersonEntityTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value ownerId = const i0.Value.absent(), + i0.Value name = const i0.Value.absent(), + i0.Value faceAssetId = const i0.Value.absent(), + i0.Value isFavorite = const i0.Value.absent(), + i0.Value isHidden = const i0.Value.absent(), + i0.Value color = const i0.Value.absent(), + i0.Value birthDate = const i0.Value.absent(), + }) => + i1.PersonEntityCompanion( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + name: name, + faceAssetId: faceAssetId, + isFavorite: isFavorite, + isHidden: isHidden, + color: color, + birthDate: birthDate, + ), + createCompanionCallback: ({ + required String id, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + required String ownerId, + required String name, + i0.Value faceAssetId = const i0.Value.absent(), + required bool isFavorite, + required bool isHidden, + i0.Value color = const i0.Value.absent(), + i0.Value birthDate = const i0.Value.absent(), + }) => + i1.PersonEntityCompanion.insert( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + name: name, + faceAssetId: faceAssetId, + isFavorite: isFavorite, + isHidden: isHidden, + color: color, + birthDate: birthDate, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$PersonEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({ownerId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (ownerId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.ownerId, + referencedTable: + i1.$$PersonEntityTableReferences._ownerIdTable(db), + referencedColumn: + i1.$$PersonEntityTableReferences._ownerIdTable(db).id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$PersonEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$PersonEntityTable, + i1.PersonEntityData, + i1.$$PersonEntityTableFilterComposer, + i1.$$PersonEntityTableOrderingComposer, + i1.$$PersonEntityTableAnnotationComposer, + $$PersonEntityTableCreateCompanionBuilder, + $$PersonEntityTableUpdateCompanionBuilder, + (i1.PersonEntityData, i1.$$PersonEntityTableReferences), + i1.PersonEntityData, + i0.PrefetchHooks Function({bool ownerId})>; + +class $PersonEntityTable extends i2.PersonEntity + with i0.TableInfo<$PersonEntityTable, i1.PersonEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $PersonEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i3.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i3.currentDateAndTime); + static const i0.VerificationMeta _ownerIdMeta = + const i0.VerificationMeta('ownerId'); + @override + late final i0.GeneratedColumn ownerId = i0.GeneratedColumn( + 'owner_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _faceAssetIdMeta = + const i0.VerificationMeta('faceAssetId'); + @override + late final i0.GeneratedColumn faceAssetId = + i0.GeneratedColumn('face_asset_id', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _isFavoriteMeta = + const i0.VerificationMeta('isFavorite'); + @override + late final i0.GeneratedColumn isFavorite = i0.GeneratedColumn( + 'is_favorite', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))')); + static const i0.VerificationMeta _isHiddenMeta = + const i0.VerificationMeta('isHidden'); + @override + late final i0.GeneratedColumn isHidden = i0.GeneratedColumn( + 'is_hidden', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))')); + static const i0.VerificationMeta _colorMeta = + const i0.VerificationMeta('color'); + @override + late final i0.GeneratedColumn color = i0.GeneratedColumn( + 'color', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _birthDateMeta = + const i0.VerificationMeta('birthDate'); + @override + late final i0.GeneratedColumn birthDate = + i0.GeneratedColumn('birth_date', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('face_asset_id')) { + context.handle( + _faceAssetIdMeta, + faceAssetId.isAcceptableOrUnknown( + data['face_asset_id']!, _faceAssetIdMeta)); + } + if (data.containsKey('is_favorite')) { + context.handle( + _isFavoriteMeta, + isFavorite.isAcceptableOrUnknown( + data['is_favorite']!, _isFavoriteMeta)); + } else if (isInserting) { + context.missing(_isFavoriteMeta); + } + if (data.containsKey('is_hidden')) { + context.handle(_isHiddenMeta, + isHidden.isAcceptableOrUnknown(data['is_hidden']!, _isHiddenMeta)); + } else if (isInserting) { + context.missing(_isHiddenMeta); + } + if (data.containsKey('color')) { + context.handle( + _colorMeta, color.isAcceptableOrUnknown(data['color']!, _colorMeta)); + } + if (data.containsKey('birth_date')) { + context.handle(_birthDateMeta, + birthDate.isAcceptableOrUnknown(data['birth_date']!, _birthDateMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.PersonEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + faceAssetId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}face_asset_id']), + isFavorite: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + isHidden: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_hidden'])!, + color: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}color']), + birthDate: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}birth_date']), + ); + } + + @override + $PersonEntityTable createAlias(String alias) { + return $PersonEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + map['owner_id'] = i0.Variable(ownerId); + map['name'] = i0.Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = i0.Variable(faceAssetId); + } + map['is_favorite'] = i0.Variable(isFavorite); + map['is_hidden'] = i0.Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = i0.Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = i0.Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + i1.PersonEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + i0.Value faceAssetId = const i0.Value.absent(), + bool? isFavorite, + bool? isHidden, + i0.Value color = const i0.Value.absent(), + i0.Value birthDate = const i0.Value.absent()}) => + i1.PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(i1.PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: + data.faceAssetId.present ? data.faceAssetId.value : this.faceAssetId, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, ownerId, name, + faceAssetId, isFavorite, isHidden, color, birthDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value ownerId; + final i0.Value name; + final i0.Value faceAssetId; + final i0.Value isFavorite; + final i0.Value isHidden; + final i0.Value color; + final i0.Value birthDate; + const PersonEntityCompanion({ + this.id = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.ownerId = const i0.Value.absent(), + this.name = const i0.Value.absent(), + this.faceAssetId = const i0.Value.absent(), + this.isFavorite = const i0.Value.absent(), + this.isHidden = const i0.Value.absent(), + this.color = const i0.Value.absent(), + this.birthDate = const i0.Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const i0.Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const i0.Value.absent(), + this.birthDate = const i0.Value.absent(), + }) : id = i0.Value(id), + ownerId = i0.Value(ownerId), + name = i0.Value(name), + isFavorite = i0.Value(isFavorite), + isHidden = i0.Value(isHidden); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? ownerId, + i0.Expression? name, + i0.Expression? faceAssetId, + i0.Expression? isFavorite, + i0.Expression? isHidden, + i0.Expression? color, + i0.Expression? birthDate, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + i1.PersonEntityCompanion copyWith( + {i0.Value? id, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? ownerId, + i0.Value? name, + i0.Value? faceAssetId, + i0.Value? isFavorite, + i0.Value? isHidden, + i0.Value? color, + i0.Value? birthDate}) { + return i1.PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = i0.Variable(ownerId.value); + } + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = i0.Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = i0.Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = i0.Variable(isHidden.value); + } + if (color.present) { + map['color'] = i0.Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = i0.Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.dart b/mobile/lib/infrastructure/entities/remote_album.entity.dart new file mode 100644 index 0000000000..74b00dd9ee --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album.entity.dart @@ -0,0 +1,31 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class RemoteAlbumEntity extends Table with DriftDefaultsMixin { + const RemoteAlbumEntity(); + + TextColumn get id => text()(); + + TextColumn get name => text()(); + + TextColumn get description => text().withDefault(const Constant(''))(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get thumbnailAssetId => + text().references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull).nullable()(); + + BoolColumn get isActivityEnabled => boolean().withDefault(const Constant(true))(); + + IntColumn get order => intEnum()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart new file mode 100644 index 0000000000..bc13c8cb5c --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart @@ -0,0 +1,946 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/album/album.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i7; + +typedef $$RemoteAlbumEntityTableCreateCompanionBuilder + = i1.RemoteAlbumEntityCompanion Function({ + required String id, + required String name, + i0.Value description, + i0.Value createdAt, + i0.Value updatedAt, + required String ownerId, + i0.Value thumbnailAssetId, + i0.Value isActivityEnabled, + required i2.AlbumAssetOrder order, +}); +typedef $$RemoteAlbumEntityTableUpdateCompanionBuilder + = i1.RemoteAlbumEntityCompanion Function({ + i0.Value id, + i0.Value name, + i0.Value description, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value ownerId, + i0.Value thumbnailAssetId, + i0.Value isActivityEnabled, + i0.Value order, +}); + +final class $$RemoteAlbumEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$RemoteAlbumEntityTable, + i1.RemoteAlbumEntityData> { + $$RemoteAlbumEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .ownerId, + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i5.$$UserEntityTableProcessedTableManager get ownerId { + final $_column = $_itemColumn('owner_id')!; + + final manager = i5 + .$$UserEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_ownerIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i7.$RemoteAssetEntityTable _thumbnailAssetIdTable( + i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .thumbnailAssetId, + i6.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .id)); + + i7.$$RemoteAssetEntityTableProcessedTableManager? get thumbnailAssetId { + final $_column = $_itemColumn('thumbnail_asset_id'); + if ($_column == null) return null; + final manager = i7 + .$$RemoteAssetEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('remote_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_thumbnailAssetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$RemoteAlbumEntityTableFilterComposer + extends i0.Composer { + $$RemoteAlbumEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get description => $composableBuilder( + column: $table.description, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isActivityEnabled => $composableBuilder( + column: $table.isActivityEnabled, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters + get order => $composableBuilder( + column: $table.order, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i5.$$UserEntityTableFilterComposer get ownerId { + final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i7.$$RemoteAssetEntityTableFilterComposer get thumbnailAssetId { + final i7.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.thumbnailAssetId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i7.$$RemoteAssetEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumEntityTableOrderingComposer + extends i0.Composer { + $$RemoteAlbumEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get description => $composableBuilder( + column: $table.description, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isActivityEnabled => $composableBuilder( + column: $table.isActivityEnabled, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get order => $composableBuilder( + column: $table.order, builder: (column) => i0.ColumnOrderings(column)); + + i5.$$UserEntityTableOrderingComposer get ownerId { + final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i7.$$RemoteAssetEntityTableOrderingComposer get thumbnailAssetId { + final i7.$$RemoteAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.thumbnailAssetId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i7.$$RemoteAssetEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumEntityTableAnnotationComposer + extends i0.Composer { + $$RemoteAlbumEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumn get description => $composableBuilder( + column: $table.description, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get isActivityEnabled => $composableBuilder( + column: $table.isActivityEnabled, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get order => + $composableBuilder(column: $table.order, builder: (column) => column); + + i5.$$UserEntityTableAnnotationComposer get ownerId { + final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i7.$$RemoteAssetEntityTableAnnotationComposer get thumbnailAssetId { + final i7.$$RemoteAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.thumbnailAssetId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i7.$$RemoteAssetEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$RemoteAlbumEntityTable, + i1.RemoteAlbumEntityData, + i1.$$RemoteAlbumEntityTableFilterComposer, + i1.$$RemoteAlbumEntityTableOrderingComposer, + i1.$$RemoteAlbumEntityTableAnnotationComposer, + $$RemoteAlbumEntityTableCreateCompanionBuilder, + $$RemoteAlbumEntityTableUpdateCompanionBuilder, + (i1.RemoteAlbumEntityData, i1.$$RemoteAlbumEntityTableReferences), + i1.RemoteAlbumEntityData, + i0.PrefetchHooks Function({bool ownerId, bool thumbnailAssetId})> { + $$RemoteAlbumEntityTableTableManager( + i0.GeneratedDatabase db, i1.$RemoteAlbumEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$RemoteAlbumEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$RemoteAlbumEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$RemoteAlbumEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value name = const i0.Value.absent(), + i0.Value description = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value ownerId = const i0.Value.absent(), + i0.Value thumbnailAssetId = const i0.Value.absent(), + i0.Value isActivityEnabled = const i0.Value.absent(), + i0.Value order = const i0.Value.absent(), + }) => + i1.RemoteAlbumEntityCompanion( + id: id, + name: name, + description: description, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + thumbnailAssetId: thumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: order, + ), + createCompanionCallback: ({ + required String id, + required String name, + i0.Value description = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + required String ownerId, + i0.Value thumbnailAssetId = const i0.Value.absent(), + i0.Value isActivityEnabled = const i0.Value.absent(), + required i2.AlbumAssetOrder order, + }) => + i1.RemoteAlbumEntityCompanion.insert( + id: id, + name: name, + description: description, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + thumbnailAssetId: thumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: order, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$RemoteAlbumEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({ownerId = false, thumbnailAssetId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (ownerId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.ownerId, + referencedTable: + i1.$$RemoteAlbumEntityTableReferences._ownerIdTable(db), + referencedColumn: i1.$$RemoteAlbumEntityTableReferences + ._ownerIdTable(db) + .id, + ) as T; + } + if (thumbnailAssetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.thumbnailAssetId, + referencedTable: i1.$$RemoteAlbumEntityTableReferences + ._thumbnailAssetIdTable(db), + referencedColumn: i1.$$RemoteAlbumEntityTableReferences + ._thumbnailAssetIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$RemoteAlbumEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$RemoteAlbumEntityTable, + i1.RemoteAlbumEntityData, + i1.$$RemoteAlbumEntityTableFilterComposer, + i1.$$RemoteAlbumEntityTableOrderingComposer, + i1.$$RemoteAlbumEntityTableAnnotationComposer, + $$RemoteAlbumEntityTableCreateCompanionBuilder, + $$RemoteAlbumEntityTableUpdateCompanionBuilder, + (i1.RemoteAlbumEntityData, i1.$$RemoteAlbumEntityTableReferences), + i1.RemoteAlbumEntityData, + i0.PrefetchHooks Function({bool ownerId, bool thumbnailAssetId})>; + +class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity + with i0.TableInfo<$RemoteAlbumEntityTable, i1.RemoteAlbumEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $RemoteAlbumEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _descriptionMeta = + const i0.VerificationMeta('description'); + @override + late final i0.GeneratedColumn description = + i0.GeneratedColumn('description', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const i4.Constant('')); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _ownerIdMeta = + const i0.VerificationMeta('ownerId'); + @override + late final i0.GeneratedColumn ownerId = i0.GeneratedColumn( + 'owner_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _thumbnailAssetIdMeta = + const i0.VerificationMeta('thumbnailAssetId'); + @override + late final i0.GeneratedColumn thumbnailAssetId = + i0.GeneratedColumn('thumbnail_asset_id', aliasedName, true, + type: i0.DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL')); + static const i0.VerificationMeta _isActivityEnabledMeta = + const i0.VerificationMeta('isActivityEnabled'); + @override + late final i0.GeneratedColumn isActivityEnabled = + i0.GeneratedColumn('is_activity_enabled', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))'), + defaultValue: const i4.Constant(true)); + @override + late final i0.GeneratedColumnWithTypeConverter + order = i0.GeneratedColumn('order', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$RemoteAlbumEntityTable.$converterorder); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('thumbnail_asset_id')) { + context.handle( + _thumbnailAssetIdMeta, + thumbnailAssetId.isAcceptableOrUnknown( + data['thumbnail_asset_id']!, _thumbnailAssetIdMeta)); + } + if (data.containsKey('is_activity_enabled')) { + context.handle( + _isActivityEnabledMeta, + isActivityEnabled.isAcceptableOrUnknown( + data['is_activity_enabled']!, _isActivityEnabledMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.RemoteAlbumEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RemoteAlbumEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}description'])!, + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}thumbnail_asset_id']), + isActivityEnabled: attachedDatabase.typeMapping.read( + i0.DriftSqlType.bool, data['${effectivePrefix}is_activity_enabled'])!, + order: i1.$RemoteAlbumEntityTable.$converterorder.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}order'])!), + ); + } + + @override + $RemoteAlbumEntityTable createAlias(String alias) { + return $RemoteAlbumEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $converterorder = + const i0.EnumIndexConverter( + i2.AlbumAssetOrder.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final i2.AlbumAssetOrder order; + const RemoteAlbumEntityData( + {required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['name'] = i0.Variable(name); + map['description'] = i0.Variable(description); + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + map['owner_id'] = i0.Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = i0.Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = i0.Variable(isActivityEnabled); + { + map['order'] = i0.Variable( + i1.$RemoteAlbumEntityTable.$converterorder.toSql(order)); + } + return map; + } + + factory RemoteAlbumEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: i1.$RemoteAlbumEntityTable.$converterorder + .fromJson(serializer.fromJson(json['order'])), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson( + i1.$RemoteAlbumEntityTable.$converterorder.toJson(order)), + }; + } + + i1.RemoteAlbumEntityData copyWith( + {String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + i0.Value thumbnailAssetId = const i0.Value.absent(), + bool? isActivityEnabled, + i2.AlbumAssetOrder? order}) => + i1.RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(i1.RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: + data.description.present ? data.description.value : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, description, createdAt, updatedAt, + ownerId, thumbnailAssetId, isActivityEnabled, order); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value name; + final i0.Value description; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value ownerId; + final i0.Value thumbnailAssetId; + final i0.Value isActivityEnabled; + final i0.Value order; + const RemoteAlbumEntityCompanion({ + this.id = const i0.Value.absent(), + this.name = const i0.Value.absent(), + this.description = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.ownerId = const i0.Value.absent(), + this.thumbnailAssetId = const i0.Value.absent(), + this.isActivityEnabled = const i0.Value.absent(), + this.order = const i0.Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + required String ownerId, + this.thumbnailAssetId = const i0.Value.absent(), + this.isActivityEnabled = const i0.Value.absent(), + required i2.AlbumAssetOrder order, + }) : id = i0.Value(id), + name = i0.Value(name), + ownerId = i0.Value(ownerId), + order = i0.Value(order); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? name, + i0.Expression? description, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? ownerId, + i0.Expression? thumbnailAssetId, + i0.Expression? isActivityEnabled, + i0.Expression? order, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + i1.RemoteAlbumEntityCompanion copyWith( + {i0.Value? id, + i0.Value? name, + i0.Value? description, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? ownerId, + i0.Value? thumbnailAssetId, + i0.Value? isActivityEnabled, + i0.Value? order}) { + return i1.RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (description.present) { + map['description'] = i0.Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = i0.Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = i0.Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = i0.Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = i0.Variable( + i1.$RemoteAlbumEntityTable.$converterorder.toSql(order.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart new file mode 100644 index 0000000000..e99f5364a4 --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart @@ -0,0 +1,15 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class RemoteAlbumAssetEntity extends Table with DriftDefaultsMixin { + const RemoteAlbumAssetEntity(); + + TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get albumId => text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {assetId, albumId}; +} diff --git a/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart new file mode 100644 index 0000000000..ab50607c96 --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart @@ -0,0 +1,565 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart' + as i2; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i3; +import 'package:drift/internal/modular.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' + as i5; + +typedef $$RemoteAlbumAssetEntityTableCreateCompanionBuilder + = i1.RemoteAlbumAssetEntityCompanion Function({ + required String assetId, + required String albumId, +}); +typedef $$RemoteAlbumAssetEntityTableUpdateCompanionBuilder + = i1.RemoteAlbumAssetEntityCompanion Function({ + i0.Value assetId, + i0.Value albumId, +}); + +final class $$RemoteAlbumAssetEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$RemoteAlbumAssetEntityTable, + i1.RemoteAlbumAssetEntityData> { + $$RemoteAlbumAssetEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet( + 'remote_album_asset_entity') + .assetId, + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .id)); + + i3.$$RemoteAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i3 + .$$RemoteAssetEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('remote_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i5.$RemoteAlbumEntityTable _albumIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet( + 'remote_album_asset_entity') + .albumId, + i4.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .id)); + + i5.$$RemoteAlbumEntityTableProcessedTableManager get albumId { + final $_column = $_itemColumn('album_id')!; + + final manager = i5 + .$$RemoteAlbumEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('remote_album_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_albumIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$RemoteAlbumAssetEntityTableFilterComposer + extends i0.Composer { + $$RemoteAlbumAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$RemoteAssetEntityTableFilterComposer get assetId { + final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$RemoteAlbumEntityTableFilterComposer get albumId { + final i5.$$RemoteAlbumEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$RemoteAlbumEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumAssetEntityTableOrderingComposer + extends i0.Composer { + $$RemoteAlbumAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$RemoteAssetEntityTableOrderingComposer get assetId { + final i3.$$RemoteAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$RemoteAlbumEntityTableOrderingComposer get albumId { + final i5.$$RemoteAlbumEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$RemoteAlbumEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumAssetEntityTableAnnotationComposer + extends i0.Composer { + $$RemoteAlbumAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$RemoteAssetEntityTableAnnotationComposer get assetId { + final i3.$$RemoteAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$RemoteAlbumEntityTableAnnotationComposer get albumId { + final i5.$$RemoteAlbumEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$RemoteAlbumEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$RemoteAlbumAssetEntityTable, + i1.RemoteAlbumAssetEntityData, + i1.$$RemoteAlbumAssetEntityTableFilterComposer, + i1.$$RemoteAlbumAssetEntityTableOrderingComposer, + i1.$$RemoteAlbumAssetEntityTableAnnotationComposer, + $$RemoteAlbumAssetEntityTableCreateCompanionBuilder, + $$RemoteAlbumAssetEntityTableUpdateCompanionBuilder, + (i1.RemoteAlbumAssetEntityData, i1.$$RemoteAlbumAssetEntityTableReferences), + i1.RemoteAlbumAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool albumId})> { + $$RemoteAlbumAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$RemoteAlbumAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$RemoteAlbumAssetEntityTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + i1.$$RemoteAlbumAssetEntityTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + i1.$$RemoteAlbumAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value assetId = const i0.Value.absent(), + i0.Value albumId = const i0.Value.absent(), + }) => + i1.RemoteAlbumAssetEntityCompanion( + assetId: assetId, + albumId: albumId, + ), + createCompanionCallback: ({ + required String assetId, + required String albumId, + }) => + i1.RemoteAlbumAssetEntityCompanion.insert( + assetId: assetId, + albumId: albumId, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$RemoteAlbumAssetEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({assetId = false, albumId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (assetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: i1.$$RemoteAlbumAssetEntityTableReferences + ._assetIdTable(db), + referencedColumn: i1.$$RemoteAlbumAssetEntityTableReferences + ._assetIdTable(db) + .id, + ) as T; + } + if (albumId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.albumId, + referencedTable: i1.$$RemoteAlbumAssetEntityTableReferences + ._albumIdTable(db), + referencedColumn: i1.$$RemoteAlbumAssetEntityTableReferences + ._albumIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$RemoteAlbumAssetEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$RemoteAlbumAssetEntityTable, + i1.RemoteAlbumAssetEntityData, + i1.$$RemoteAlbumAssetEntityTableFilterComposer, + i1.$$RemoteAlbumAssetEntityTableOrderingComposer, + i1.$$RemoteAlbumAssetEntityTableAnnotationComposer, + $$RemoteAlbumAssetEntityTableCreateCompanionBuilder, + $$RemoteAlbumAssetEntityTableUpdateCompanionBuilder, + ( + i1.RemoteAlbumAssetEntityData, + i1.$$RemoteAlbumAssetEntityTableReferences + ), + i1.RemoteAlbumAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool albumId})>; + +class $RemoteAlbumAssetEntityTable extends i2.RemoteAlbumAssetEntity + with + i0.TableInfo<$RemoteAlbumAssetEntityTable, + i1.RemoteAlbumAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $RemoteAlbumAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _assetIdMeta = + const i0.VerificationMeta('assetId'); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _albumIdMeta = + const i0.VerificationMeta('albumId'); + @override + late final i0.GeneratedColumn albumId = i0.GeneratedColumn( + 'album_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('asset_id')) { + context.handle(_assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + if (data.containsKey('album_id')) { + context.handle(_albumIdMeta, + albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta)); + } else if (isInserting) { + context.missing(_albumIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {assetId, albumId}; + @override + i1.RemoteAlbumAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + $RemoteAlbumAssetEntityTable createAlias(String alias) { + return $RemoteAlbumAssetEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData( + {required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = i0.Variable(assetId); + map['album_id'] = i0.Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + i1.RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + i1.RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + i1.RemoteAlbumAssetEntityCompanion data) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value assetId; + final i0.Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const i0.Value.absent(), + this.albumId = const i0.Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = i0.Value(assetId), + albumId = i0.Value(albumId); + static i0.Insertable custom({ + i0.Expression? assetId, + i0.Expression? albumId, + }) { + return i0.RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + i1.RemoteAlbumAssetEntityCompanion copyWith( + {i0.Value? assetId, i0.Value? albumId}) { + return i1.RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = i0.Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/remote_album_user.entity.dart b/mobile/lib/infrastructure/entities/remote_album_user.entity.dart new file mode 100644 index 0000000000..9cb277f9d0 --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album_user.entity.dart @@ -0,0 +1,18 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class RemoteAlbumUserEntity extends Table with DriftDefaultsMixin { + const RemoteAlbumUserEntity(); + + TextColumn get albumId => text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get userId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + IntColumn get role => intEnum()(); + + @override + Set get primaryKey => {albumId, userId}; +} diff --git a/mobile/lib/infrastructure/entities/remote_album_user.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album_user.entity.drift.dart new file mode 100644 index 0000000000..7ec1151a8a --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album_user.entity.drift.dart @@ -0,0 +1,618 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/album/album.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart' + as i3; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' + as i4; +import 'package:drift/internal/modular.dart' as i5; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i6; + +typedef $$RemoteAlbumUserEntityTableCreateCompanionBuilder + = i1.RemoteAlbumUserEntityCompanion Function({ + required String albumId, + required String userId, + required i2.AlbumUserRole role, +}); +typedef $$RemoteAlbumUserEntityTableUpdateCompanionBuilder + = i1.RemoteAlbumUserEntityCompanion Function({ + i0.Value albumId, + i0.Value userId, + i0.Value role, +}); + +final class $$RemoteAlbumUserEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$RemoteAlbumUserEntityTable, + i1.RemoteAlbumUserEntityData> { + $$RemoteAlbumUserEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i4.$RemoteAlbumEntityTable _albumIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .createAlias(i0.$_aliasNameGenerator( + i5.ReadDatabaseContainer(db) + .resultSet( + 'remote_album_user_entity') + .albumId, + i5.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .id)); + + i4.$$RemoteAlbumEntityTableProcessedTableManager get albumId { + final $_column = $_itemColumn('album_id')!; + + final manager = i4 + .$$RemoteAlbumEntityTableTableManager( + $_db, + i5.ReadDatabaseContainer($_db) + .resultSet('remote_album_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_albumIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i6.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i5.ReadDatabaseContainer(db) + .resultSet( + 'remote_album_user_entity') + .userId, + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i6.$$UserEntityTableProcessedTableManager get userId { + final $_column = $_itemColumn('user_id')!; + + final manager = i6 + .$$UserEntityTableTableManager( + $_db, + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_userIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$RemoteAlbumUserEntityTableFilterComposer + extends i0.Composer { + $$RemoteAlbumUserEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnWithTypeConverterFilters + get role => $composableBuilder( + column: $table.role, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i4.$$RemoteAlbumEntityTableFilterComposer get albumId { + final i4.$$RemoteAlbumEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$RemoteAlbumEntityTableFilterComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i6.$$UserEntityTableFilterComposer get userId { + final i6.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i6.$$UserEntityTableFilterComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumUserEntityTableOrderingComposer + extends i0.Composer { + $$RemoteAlbumUserEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get role => $composableBuilder( + column: $table.role, builder: (column) => i0.ColumnOrderings(column)); + + i4.$$RemoteAlbumEntityTableOrderingComposer get albumId { + final i4.$$RemoteAlbumEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$RemoteAlbumEntityTableOrderingComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet( + 'remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i6.$$UserEntityTableOrderingComposer get userId { + final i6.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i6.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumUserEntityTableAnnotationComposer + extends i0.Composer { + $$RemoteAlbumUserEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumnWithTypeConverter get role => + $composableBuilder(column: $table.role, builder: (column) => column); + + i4.$$RemoteAlbumEntityTableAnnotationComposer get albumId { + final i4.$$RemoteAlbumEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$RemoteAlbumEntityTableAnnotationComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet( + 'remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i6.$$UserEntityTableAnnotationComposer get userId { + final i6.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i6.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumUserEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$RemoteAlbumUserEntityTable, + i1.RemoteAlbumUserEntityData, + i1.$$RemoteAlbumUserEntityTableFilterComposer, + i1.$$RemoteAlbumUserEntityTableOrderingComposer, + i1.$$RemoteAlbumUserEntityTableAnnotationComposer, + $$RemoteAlbumUserEntityTableCreateCompanionBuilder, + $$RemoteAlbumUserEntityTableUpdateCompanionBuilder, + (i1.RemoteAlbumUserEntityData, i1.$$RemoteAlbumUserEntityTableReferences), + i1.RemoteAlbumUserEntityData, + i0.PrefetchHooks Function({bool albumId, bool userId})> { + $$RemoteAlbumUserEntityTableTableManager( + i0.GeneratedDatabase db, i1.$RemoteAlbumUserEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$RemoteAlbumUserEntityTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + i1.$$RemoteAlbumUserEntityTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + i1.$$RemoteAlbumUserEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value albumId = const i0.Value.absent(), + i0.Value userId = const i0.Value.absent(), + i0.Value role = const i0.Value.absent(), + }) => + i1.RemoteAlbumUserEntityCompanion( + albumId: albumId, + userId: userId, + role: role, + ), + createCompanionCallback: ({ + required String albumId, + required String userId, + required i2.AlbumUserRole role, + }) => + i1.RemoteAlbumUserEntityCompanion.insert( + albumId: albumId, + userId: userId, + role: role, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$RemoteAlbumUserEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({albumId = false, userId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (albumId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.albumId, + referencedTable: i1.$$RemoteAlbumUserEntityTableReferences + ._albumIdTable(db), + referencedColumn: i1.$$RemoteAlbumUserEntityTableReferences + ._albumIdTable(db) + .id, + ) as T; + } + if (userId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.userId, + referencedTable: i1.$$RemoteAlbumUserEntityTableReferences + ._userIdTable(db), + referencedColumn: i1.$$RemoteAlbumUserEntityTableReferences + ._userIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$RemoteAlbumUserEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$RemoteAlbumUserEntityTable, + i1.RemoteAlbumUserEntityData, + i1.$$RemoteAlbumUserEntityTableFilterComposer, + i1.$$RemoteAlbumUserEntityTableOrderingComposer, + i1.$$RemoteAlbumUserEntityTableAnnotationComposer, + $$RemoteAlbumUserEntityTableCreateCompanionBuilder, + $$RemoteAlbumUserEntityTableUpdateCompanionBuilder, + ( + i1.RemoteAlbumUserEntityData, + i1.$$RemoteAlbumUserEntityTableReferences + ), + i1.RemoteAlbumUserEntityData, + i0.PrefetchHooks Function({bool albumId, bool userId})>; + +class $RemoteAlbumUserEntityTable extends i3.RemoteAlbumUserEntity + with + i0 + .TableInfo<$RemoteAlbumUserEntityTable, i1.RemoteAlbumUserEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $RemoteAlbumUserEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _albumIdMeta = + const i0.VerificationMeta('albumId'); + @override + late final i0.GeneratedColumn albumId = i0.GeneratedColumn( + 'album_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _userIdMeta = + const i0.VerificationMeta('userId'); + @override + late final i0.GeneratedColumn userId = i0.GeneratedColumn( + 'user_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + @override + late final i0.GeneratedColumnWithTypeConverter role = + i0.GeneratedColumn('role', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$RemoteAlbumUserEntityTable.$converterrole); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('album_id')) { + context.handle(_albumIdMeta, + albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta)); + } else if (isInserting) { + context.missing(_albumIdMeta); + } + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + } else if (isInserting) { + context.missing(_userIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {albumId, userId}; + @override + i1.RemoteAlbumUserEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}album_id'])!, + userId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!, + role: i1.$RemoteAlbumUserEntityTable.$converterrole.fromSql( + attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}role'])!), + ); + } + + @override + $RemoteAlbumUserEntityTable createAlias(String alias) { + return $RemoteAlbumUserEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $converterrole = + const i0.EnumIndexConverter(i2.AlbumUserRole.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends i0.DataClass + implements i0.Insertable { + final String albumId; + final String userId; + final i2.AlbumUserRole role; + const RemoteAlbumUserEntityData( + {required this.albumId, required this.userId, required this.role}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = i0.Variable(albumId); + map['user_id'] = i0.Variable(userId); + { + map['role'] = i0.Variable( + i1.$RemoteAlbumUserEntityTable.$converterrole.toSql(role)); + } + return map; + } + + factory RemoteAlbumUserEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: i1.$RemoteAlbumUserEntityTable.$converterrole + .fromJson(serializer.fromJson(json['role'])), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson( + i1.$RemoteAlbumUserEntityTable.$converterrole.toJson(role)), + }; + } + + i1.RemoteAlbumUserEntityData copyWith( + {String? albumId, String? userId, i2.AlbumUserRole? role}) => + i1.RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + i1.RemoteAlbumUserEntityCompanion data) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends i0.UpdateCompanion { + final i0.Value albumId; + final i0.Value userId; + final i0.Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const i0.Value.absent(), + this.userId = const i0.Value.absent(), + this.role = const i0.Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required i2.AlbumUserRole role, + }) : albumId = i0.Value(albumId), + userId = i0.Value(userId), + role = i0.Value(role); + static i0.Insertable custom({ + i0.Expression? albumId, + i0.Expression? userId, + i0.Expression? role, + }) { + return i0.RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + i1.RemoteAlbumUserEntityCompanion copyWith( + {i0.Value? albumId, + i0.Value? userId, + i0.Value? role}) { + return i1.RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = i0.Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = i0.Variable(userId.value); + } + if (role.present) { + map['role'] = i0.Variable( + i1.$RemoteAlbumUserEntityTable.$converterrole.toSql(role.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index 3c7589949f..cb14e2501d 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -1,5 +1,6 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; @@ -10,8 +11,7 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; unique: true, ) @TableIndex(name: 'idx_remote_asset_checksum', columns: {#checksum}) -class RemoteAssetEntity extends Table - with DriftDefaultsMixin, AssetEntityMixin { +class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { const RemoteAssetEntity(); TextColumn get id => text()(); @@ -20,8 +20,7 @@ class RemoteAssetEntity extends Table BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); - TextColumn get ownerId => - text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); DateTimeColumn get localDateTime => dateTime().nullable()(); @@ -29,8 +28,33 @@ class RemoteAssetEntity extends Table DateTimeColumn get deletedAt => dateTime().nullable()(); + TextColumn get livePhotoVideoId => text().nullable()(); + IntColumn get visibility => intEnum()(); + TextColumn get stackId => text().nullable()(); + @override Set get primaryKey => {id}; } + +extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData { + RemoteAsset toDto() => RemoteAsset( + id: id, + name: name, + ownerId: ownerId, + checksum: checksum, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + isFavorite: isFavorite, + height: height, + width: width, + thumbHash: thumbHash, + visibility: visibility, + livePhotoVideoId: livePhotoVideoId, + localId: null, + stackId: stackId, + ); +} diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart index 4a13b74f5d..543ed65985 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart @@ -27,7 +27,9 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder i0.Value localDateTime, i0.Value thumbHash, i0.Value deletedAt, + i0.Value livePhotoVideoId, required i2.AssetVisibility visibility, + i0.Value stackId, }); typedef $$RemoteAssetEntityTableUpdateCompanionBuilder = i1.RemoteAssetEntityCompanion Function({ @@ -45,7 +47,9 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder i0.Value localDateTime, i0.Value thumbHash, i0.Value deletedAt, + i0.Value livePhotoVideoId, i0.Value visibility, + i0.Value stackId, }); final class $$RemoteAssetEntityTableReferences extends i0.BaseReferences< @@ -134,11 +138,18 @@ class $$RemoteAssetEntityTableFilterComposer i0.ColumnFilters get deletedAt => $composableBuilder( column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column)); + i0.ColumnFilters get livePhotoVideoId => $composableBuilder( + column: $table.livePhotoVideoId, + builder: (column) => i0.ColumnFilters(column)); + i0.ColumnWithTypeConverterFilters get visibility => $composableBuilder( column: $table.visibility, builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + i0.ColumnFilters get stackId => $composableBuilder( + column: $table.stackId, builder: (column) => i0.ColumnFilters(column)); + i5.$$UserEntityTableFilterComposer get ownerId { final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, @@ -217,10 +228,17 @@ class $$RemoteAssetEntityTableOrderingComposer column: $table.deletedAt, builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get livePhotoVideoId => $composableBuilder( + column: $table.livePhotoVideoId, + builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get visibility => $composableBuilder( column: $table.visibility, builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get stackId => $composableBuilder( + column: $table.stackId, builder: (column) => i0.ColumnOrderings(column)); + i5.$$UserEntityTableOrderingComposer get ownerId { final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, @@ -292,10 +310,16 @@ class $$RemoteAssetEntityTableAnnotationComposer i0.GeneratedColumn get deletedAt => $composableBuilder(column: $table.deletedAt, builder: (column) => column); + i0.GeneratedColumn get livePhotoVideoId => $composableBuilder( + column: $table.livePhotoVideoId, builder: (column) => column); + i0.GeneratedColumnWithTypeConverter get visibility => $composableBuilder( column: $table.visibility, builder: (column) => column); + i0.GeneratedColumn get stackId => + $composableBuilder(column: $table.stackId, builder: (column) => column); + i5.$$UserEntityTableAnnotationComposer get ownerId { final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -358,7 +382,9 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< i0.Value localDateTime = const i0.Value.absent(), i0.Value thumbHash = const i0.Value.absent(), i0.Value deletedAt = const i0.Value.absent(), + i0.Value livePhotoVideoId = const i0.Value.absent(), i0.Value visibility = const i0.Value.absent(), + i0.Value stackId = const i0.Value.absent(), }) => i1.RemoteAssetEntityCompanion( name: name, @@ -375,7 +401,9 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< localDateTime: localDateTime, thumbHash: thumbHash, deletedAt: deletedAt, + livePhotoVideoId: livePhotoVideoId, visibility: visibility, + stackId: stackId, ), createCompanionCallback: ({ required String name, @@ -392,7 +420,9 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< i0.Value localDateTime = const i0.Value.absent(), i0.Value thumbHash = const i0.Value.absent(), i0.Value deletedAt = const i0.Value.absent(), + i0.Value livePhotoVideoId = const i0.Value.absent(), required i2.AssetVisibility visibility, + i0.Value stackId = const i0.Value.absent(), }) => i1.RemoteAssetEntityCompanion.insert( name: name, @@ -409,7 +439,9 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< localDateTime: localDateTime, thumbHash: thumbHash, deletedAt: deletedAt, + livePhotoVideoId: livePhotoVideoId, visibility: visibility, + stackId: stackId, ), withReferenceMapper: (p0) => p0 .map((e) => ( @@ -573,12 +605,24 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity late final i0.GeneratedColumn deletedAt = i0.GeneratedColumn('deleted_at', aliasedName, true, type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _livePhotoVideoIdMeta = + const i0.VerificationMeta('livePhotoVideoId'); + @override + late final i0.GeneratedColumn livePhotoVideoId = + i0.GeneratedColumn('live_photo_video_id', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); @override late final i0.GeneratedColumnWithTypeConverter visibility = i0.GeneratedColumn('visibility', aliasedName, false, type: i0.DriftSqlType.int, requiredDuringInsert: true) .withConverter( i1.$RemoteAssetEntityTable.$convertervisibility); + static const i0.VerificationMeta _stackIdMeta = + const i0.VerificationMeta('stackId'); + @override + late final i0.GeneratedColumn stackId = i0.GeneratedColumn( + 'stack_id', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); @override List get $columns => [ name, @@ -595,7 +639,9 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity localDateTime, thumbHash, deletedAt, - visibility + livePhotoVideoId, + visibility, + stackId ]; @override String get aliasedName => _alias ?? actualTableName; @@ -673,6 +719,16 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity context.handle(_deletedAtMeta, deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta)); } + if (data.containsKey('live_photo_video_id')) { + context.handle( + _livePhotoVideoIdMeta, + livePhotoVideoId.isAcceptableOrUnknown( + data['live_photo_video_id']!, _livePhotoVideoIdMeta)); + } + if (data.containsKey('stack_id')) { + context.handle(_stackIdMeta, + stackId.isAcceptableOrUnknown(data['stack_id']!, _stackIdMeta)); + } return context; } @@ -712,9 +768,14 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity .read(i0.DriftSqlType.string, data['${effectivePrefix}thumb_hash']), deletedAt: attachedDatabase.typeMapping .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + livePhotoVideoId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id']), visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql( attachedDatabase.typeMapping.read( i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!), + stackId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}stack_id']), ); } @@ -750,7 +811,9 @@ class RemoteAssetEntityData extends i0.DataClass final DateTime? localDateTime; final String? thumbHash; final DateTime? deletedAt; + final String? livePhotoVideoId; final i2.AssetVisibility visibility; + final String? stackId; const RemoteAssetEntityData( {required this.name, required this.type, @@ -766,7 +829,9 @@ class RemoteAssetEntityData extends i0.DataClass this.localDateTime, this.thumbHash, this.deletedAt, - required this.visibility}); + this.livePhotoVideoId, + required this.visibility, + this.stackId}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -799,10 +864,16 @@ class RemoteAssetEntityData extends i0.DataClass if (!nullToAbsent || deletedAt != null) { map['deleted_at'] = i0.Variable(deletedAt); } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = i0.Variable(livePhotoVideoId); + } { map['visibility'] = i0.Variable( i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility)); } + if (!nullToAbsent || stackId != null) { + map['stack_id'] = i0.Variable(stackId); + } return map; } @@ -825,8 +896,10 @@ class RemoteAssetEntityData extends i0.DataClass localDateTime: serializer.fromJson(json['localDateTime']), thumbHash: serializer.fromJson(json['thumbHash']), deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), visibility: i1.$RemoteAssetEntityTable.$convertervisibility .fromJson(serializer.fromJson(json['visibility'])), + stackId: serializer.fromJson(json['stackId']), ); } @override @@ -848,8 +921,10 @@ class RemoteAssetEntityData extends i0.DataClass 'localDateTime': serializer.toJson(localDateTime), 'thumbHash': serializer.toJson(thumbHash), 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), 'visibility': serializer.toJson( i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)), + 'stackId': serializer.toJson(stackId), }; } @@ -868,7 +943,9 @@ class RemoteAssetEntityData extends i0.DataClass i0.Value localDateTime = const i0.Value.absent(), i0.Value thumbHash = const i0.Value.absent(), i0.Value deletedAt = const i0.Value.absent(), - i2.AssetVisibility? visibility}) => + i0.Value livePhotoVideoId = const i0.Value.absent(), + i2.AssetVisibility? visibility, + i0.Value stackId = const i0.Value.absent()}) => i1.RemoteAssetEntityData( name: name ?? this.name, type: type ?? this.type, @@ -887,7 +964,11 @@ class RemoteAssetEntityData extends i0.DataClass localDateTime.present ? localDateTime.value : this.localDateTime, thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, ); RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) { return RemoteAssetEntityData( @@ -910,8 +991,12 @@ class RemoteAssetEntityData extends i0.DataClass : this.localDateTime, thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, visibility: data.visibility.present ? data.visibility.value : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, ); } @@ -932,7 +1017,9 @@ class RemoteAssetEntityData extends i0.DataClass ..write('localDateTime: $localDateTime, ') ..write('thumbHash: $thumbHash, ') ..write('deletedAt: $deletedAt, ') - ..write('visibility: $visibility') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') ..write(')')) .toString(); } @@ -953,7 +1040,9 @@ class RemoteAssetEntityData extends i0.DataClass localDateTime, thumbHash, deletedAt, - visibility); + livePhotoVideoId, + visibility, + stackId); @override bool operator ==(Object other) => identical(this, other) || @@ -972,7 +1061,9 @@ class RemoteAssetEntityData extends i0.DataClass other.localDateTime == this.localDateTime && other.thumbHash == this.thumbHash && other.deletedAt == this.deletedAt && - other.visibility == this.visibility); + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId); } class RemoteAssetEntityCompanion @@ -991,7 +1082,9 @@ class RemoteAssetEntityCompanion final i0.Value localDateTime; final i0.Value thumbHash; final i0.Value deletedAt; + final i0.Value livePhotoVideoId; final i0.Value visibility; + final i0.Value stackId; const RemoteAssetEntityCompanion({ this.name = const i0.Value.absent(), this.type = const i0.Value.absent(), @@ -1007,7 +1100,9 @@ class RemoteAssetEntityCompanion this.localDateTime = const i0.Value.absent(), this.thumbHash = const i0.Value.absent(), this.deletedAt = const i0.Value.absent(), + this.livePhotoVideoId = const i0.Value.absent(), this.visibility = const i0.Value.absent(), + this.stackId = const i0.Value.absent(), }); RemoteAssetEntityCompanion.insert({ required String name, @@ -1024,7 +1119,9 @@ class RemoteAssetEntityCompanion this.localDateTime = const i0.Value.absent(), this.thumbHash = const i0.Value.absent(), this.deletedAt = const i0.Value.absent(), + this.livePhotoVideoId = const i0.Value.absent(), required i2.AssetVisibility visibility, + this.stackId = const i0.Value.absent(), }) : name = i0.Value(name), type = i0.Value(type), id = i0.Value(id), @@ -1046,7 +1143,9 @@ class RemoteAssetEntityCompanion i0.Expression? localDateTime, i0.Expression? thumbHash, i0.Expression? deletedAt, + i0.Expression? livePhotoVideoId, i0.Expression? visibility, + i0.Expression? stackId, }) { return i0.RawValuesInsertable({ if (name != null) 'name': name, @@ -1063,7 +1162,9 @@ class RemoteAssetEntityCompanion if (localDateTime != null) 'local_date_time': localDateTime, if (thumbHash != null) 'thumb_hash': thumbHash, if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, }); } @@ -1082,7 +1183,9 @@ class RemoteAssetEntityCompanion i0.Value? localDateTime, i0.Value? thumbHash, i0.Value? deletedAt, - i0.Value? visibility}) { + i0.Value? livePhotoVideoId, + i0.Value? visibility, + i0.Value? stackId}) { return i1.RemoteAssetEntityCompanion( name: name ?? this.name, type: type ?? this.type, @@ -1098,7 +1201,9 @@ class RemoteAssetEntityCompanion localDateTime: localDateTime ?? this.localDateTime, thumbHash: thumbHash ?? this.thumbHash, deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, ); } @@ -1148,11 +1253,17 @@ class RemoteAssetEntityCompanion if (deletedAt.present) { map['deleted_at'] = i0.Variable(deletedAt.value); } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = i0.Variable(livePhotoVideoId.value); + } if (visibility.present) { map['visibility'] = i0.Variable(i1 .$RemoteAssetEntityTable.$convertervisibility .toSql(visibility.value)); } + if (stackId.present) { + map['stack_id'] = i0.Variable(stackId.value); + } return map; } @@ -1173,7 +1284,9 @@ class RemoteAssetEntityCompanion ..write('localDateTime: $localDateTime, ') ..write('thumbHash: $thumbHash, ') ..write('deletedAt: $deletedAt, ') - ..write('visibility: $visibility') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') ..write(')')) .toString(); } diff --git a/mobile/lib/infrastructure/entities/stack.entity.dart b/mobile/lib/infrastructure/entities/stack.entity.dart new file mode 100644 index 0000000000..be50d7e330 --- /dev/null +++ b/mobile/lib/infrastructure/entities/stack.entity.dart @@ -0,0 +1,20 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class StackEntity extends Table with DriftDefaultsMixin { + const StackEntity(); + + TextColumn get id => text()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get primaryAssetId => text()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/stack.entity.drift.dart b/mobile/lib/infrastructure/entities/stack.entity.drift.dart new file mode 100644 index 0000000000..df0390aea0 --- /dev/null +++ b/mobile/lib/infrastructure/entities/stack.entity.drift.dart @@ -0,0 +1,603 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/stack.entity.dart' as i2; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i4; +import 'package:drift/internal/modular.dart' as i5; + +typedef $$StackEntityTableCreateCompanionBuilder = i1.StackEntityCompanion + Function({ + required String id, + i0.Value createdAt, + i0.Value updatedAt, + required String ownerId, + required String primaryAssetId, +}); +typedef $$StackEntityTableUpdateCompanionBuilder = i1.StackEntityCompanion + Function({ + i0.Value id, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value ownerId, + i0.Value primaryAssetId, +}); + +final class $$StackEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, i1.$StackEntityTable, i1.StackEntityData> { + $$StackEntityTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static i4.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i5.ReadDatabaseContainer(db) + .resultSet('stack_entity') + .ownerId, + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i4.$$UserEntityTableProcessedTableManager get ownerId { + final $_column = $_itemColumn('owner_id')!; + + final manager = i4 + .$$UserEntityTableTableManager( + $_db, + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_ownerIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$StackEntityTableFilterComposer + extends i0.Composer { + $$StackEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get primaryAssetId => $composableBuilder( + column: $table.primaryAssetId, + builder: (column) => i0.ColumnFilters(column)); + + i4.$$UserEntityTableFilterComposer get ownerId { + final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$UserEntityTableFilterComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$StackEntityTableOrderingComposer + extends i0.Composer { + $$StackEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get primaryAssetId => $composableBuilder( + column: $table.primaryAssetId, + builder: (column) => i0.ColumnOrderings(column)); + + i4.$$UserEntityTableOrderingComposer get ownerId { + final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$StackEntityTableAnnotationComposer + extends i0.Composer { + $$StackEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get primaryAssetId => $composableBuilder( + column: $table.primaryAssetId, builder: (column) => column); + + i4.$$UserEntityTableAnnotationComposer get ownerId { + final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$StackEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$StackEntityTable, + i1.StackEntityData, + i1.$$StackEntityTableFilterComposer, + i1.$$StackEntityTableOrderingComposer, + i1.$$StackEntityTableAnnotationComposer, + $$StackEntityTableCreateCompanionBuilder, + $$StackEntityTableUpdateCompanionBuilder, + (i1.StackEntityData, i1.$$StackEntityTableReferences), + i1.StackEntityData, + i0.PrefetchHooks Function({bool ownerId})> { + $$StackEntityTableTableManager( + i0.GeneratedDatabase db, i1.$StackEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$StackEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$StackEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$StackEntityTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value ownerId = const i0.Value.absent(), + i0.Value primaryAssetId = const i0.Value.absent(), + }) => + i1.StackEntityCompanion( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + primaryAssetId: primaryAssetId, + ), + createCompanionCallback: ({ + required String id, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + required String ownerId, + required String primaryAssetId, + }) => + i1.StackEntityCompanion.insert( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + primaryAssetId: primaryAssetId, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$StackEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({ownerId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (ownerId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.ownerId, + referencedTable: + i1.$$StackEntityTableReferences._ownerIdTable(db), + referencedColumn: + i1.$$StackEntityTableReferences._ownerIdTable(db).id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$StackEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$StackEntityTable, + i1.StackEntityData, + i1.$$StackEntityTableFilterComposer, + i1.$$StackEntityTableOrderingComposer, + i1.$$StackEntityTableAnnotationComposer, + $$StackEntityTableCreateCompanionBuilder, + $$StackEntityTableUpdateCompanionBuilder, + (i1.StackEntityData, i1.$$StackEntityTableReferences), + i1.StackEntityData, + i0.PrefetchHooks Function({bool ownerId})>; + +class $StackEntityTable extends i2.StackEntity + with i0.TableInfo<$StackEntityTable, i1.StackEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $StackEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i3.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i3.currentDateAndTime); + static const i0.VerificationMeta _ownerIdMeta = + const i0.VerificationMeta('ownerId'); + @override + late final i0.GeneratedColumn ownerId = i0.GeneratedColumn( + 'owner_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _primaryAssetIdMeta = + const i0.VerificationMeta('primaryAssetId'); + @override + late final i0.GeneratedColumn primaryAssetId = + i0.GeneratedColumn('primary_asset_id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => + [id, createdAt, updatedAt, ownerId, primaryAssetId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('primary_asset_id')) { + context.handle( + _primaryAssetIdMeta, + primaryAssetId.isAcceptableOrUnknown( + data['primary_asset_id']!, _primaryAssetIdMeta)); + } else if (isInserting) { + context.missing(_primaryAssetIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.StackEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + primaryAssetId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!, + ); + } + + @override + $StackEntityTable createAlias(String alias) { + return $StackEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + map['owner_id'] = i0.Variable(ownerId); + map['primary_asset_id'] = i0.Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + i1.StackEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId}) => + i1.StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(i1.StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value ownerId; + final i0.Value primaryAssetId; + const StackEntityCompanion({ + this.id = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.ownerId = const i0.Value.absent(), + this.primaryAssetId = const i0.Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = i0.Value(id), + ownerId = i0.Value(ownerId), + primaryAssetId = i0.Value(primaryAssetId); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? ownerId, + i0.Expression? primaryAssetId, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + i1.StackEntityCompanion copyWith( + {i0.Value? id, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? ownerId, + i0.Value? primaryAssetId}) { + return i1.StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = i0.Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = i0.Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.dart index 302a9ffce1..ede3de3966 100644 --- a/mobile/lib/infrastructure/entities/user_metadata.entity.dart +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.dart @@ -6,16 +6,16 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class UserMetadataEntity extends Table with DriftDefaultsMixin { const UserMetadataEntity(); - TextColumn get userId => - text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); - TextColumn get preferences => text().map(userPreferenceConverter)(); + TextColumn get userId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + IntColumn get key => intEnum()(); + + BlobColumn get value => blob().map(userMetadataConverter)(); @override - Set get primaryKey => {userId}; + Set get primaryKey => {userId, key}; } -final JsonTypeConverter2 - userPreferenceConverter = TypeConverter.json2( - fromJson: (json) => UserPreferences.fromMap(json as Map), - toJson: (pref) => pref.toMap(), +final JsonTypeConverter2, Uint8List, Object?> userMetadataConverter = TypeConverter.jsonb( + fromJson: (json) => json as Map, ); diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart index 95ab63ebf6..a13ea5c04e 100644 --- a/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart @@ -4,21 +4,24 @@ import 'package:drift/drift.dart' as i0; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' as i1; import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i2; +import 'dart:typed_data' as i3; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart' - as i3; -import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' as i4; -import 'package:drift/internal/modular.dart' as i5; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; typedef $$UserMetadataEntityTableCreateCompanionBuilder = i1.UserMetadataEntityCompanion Function({ required String userId, - required i2.UserPreferences preferences, + required i2.UserMetadataKey key, + required Map value, }); typedef $$UserMetadataEntityTableUpdateCompanionBuilder = i1.UserMetadataEntityCompanion Function({ i0.Value userId, - i0.Value preferences, + i0.Value key, + i0.Value> value, }); final class $$UserMetadataEntityTableReferences extends i0.BaseReferences< @@ -28,26 +31,26 @@ final class $$UserMetadataEntityTableReferences extends i0.BaseReferences< $$UserMetadataEntityTableReferences( super.$_db, super.$_table, super.$_typedResult); - static i4.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) => - i5.ReadDatabaseContainer(db) - .resultSet('user_entity') + static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') .createAlias(i0.$_aliasNameGenerator( - i5.ReadDatabaseContainer(db) + i6.ReadDatabaseContainer(db) .resultSet( 'user_metadata_entity') .userId, - i5.ReadDatabaseContainer(db) - .resultSet('user_entity') + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') .id)); - i4.$$UserEntityTableProcessedTableManager get userId { + i5.$$UserEntityTableProcessedTableManager get userId { final $_column = $_itemColumn('user_id')!; - final manager = i4 + final manager = i5 .$$UserEntityTableTableManager( $_db, - i5.ReadDatabaseContainer($_db) - .resultSet('user_entity')) + i6.ReadDatabaseContainer($_db) + .resultSet('user_entity')) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_userIdTable($_db)); if (item == null) return manager; @@ -65,26 +68,31 @@ class $$UserMetadataEntityTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnWithTypeConverterFilters - get preferences => $composableBuilder( - column: $table.preferences, + i0.ColumnWithTypeConverterFilters + get key => $composableBuilder( + column: $table.key, builder: (column) => i0.ColumnWithTypeConverterFilters(column)); - i4.$$UserEntityTableFilterComposer get userId { - final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( + i0.ColumnWithTypeConverterFilters, Map, + i3.Uint8List> + get value => $composableBuilder( + column: $table.value, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i5.$$UserEntityTableFilterComposer get userId { + final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.userId, - referencedTable: i5.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i4.$$UserEntityTableFilterComposer( + i5.$$UserEntityTableFilterComposer( $db: $db, - $table: i5.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -103,24 +111,26 @@ class $$UserMetadataEntityTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnOrderings get preferences => $composableBuilder( - column: $table.preferences, - builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get key => $composableBuilder( + column: $table.key, builder: (column) => i0.ColumnOrderings(column)); - i4.$$UserEntityTableOrderingComposer get userId { - final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( + i0.ColumnOrderings get value => $composableBuilder( + column: $table.value, builder: (column) => i0.ColumnOrderings(column)); + + i5.$$UserEntityTableOrderingComposer get userId { + final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.userId, - referencedTable: i5.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i4.$$UserEntityTableOrderingComposer( + i5.$$UserEntityTableOrderingComposer( $db: $db, - $table: i5.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -139,24 +149,27 @@ class $$UserMetadataEntityTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.GeneratedColumnWithTypeConverter - get preferences => $composableBuilder( - column: $table.preferences, builder: (column) => column); + i0.GeneratedColumnWithTypeConverter get key => + $composableBuilder(column: $table.key, builder: (column) => column); - i4.$$UserEntityTableAnnotationComposer get userId { - final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + i0.GeneratedColumnWithTypeConverter, i3.Uint8List> + get value => + $composableBuilder(column: $table.value, builder: (column) => column); + + i5.$$UserEntityTableAnnotationComposer get userId { + final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.userId, - referencedTable: i5.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i4.$$UserEntityTableAnnotationComposer( + i5.$$UserEntityTableAnnotationComposer( $db: $db, - $table: i5.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -193,19 +206,23 @@ class $$UserMetadataEntityTableTableManager extends i0.RootTableManager< $db: db, $table: table), updateCompanionCallback: ({ i0.Value userId = const i0.Value.absent(), - i0.Value preferences = const i0.Value.absent(), + i0.Value key = const i0.Value.absent(), + i0.Value> value = const i0.Value.absent(), }) => i1.UserMetadataEntityCompanion( userId: userId, - preferences: preferences, + key: key, + value: value, ), createCompanionCallback: ({ required String userId, - required i2.UserPreferences preferences, + required i2.UserMetadataKey key, + required Map value, }) => i1.UserMetadataEntityCompanion.insert( userId: userId, - preferences: preferences, + key: key, + value: value, ), withReferenceMapper: (p0) => p0 .map((e) => ( @@ -266,7 +283,7 @@ typedef $$UserMetadataEntityTableProcessedTableManager i1.UserMetadataEntityData, i0.PrefetchHooks Function({bool userId})>; -class $UserMetadataEntityTable extends i3.UserMetadataEntity +class $UserMetadataEntityTable extends i4.UserMetadataEntity with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> { @override final i0.GeneratedDatabase attachedDatabase; @@ -282,14 +299,20 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity defaultConstraints: i0.GeneratedColumn.constraintIsAlways( 'REFERENCES user_entity (id) ON DELETE CASCADE')); @override - late final i0.GeneratedColumnWithTypeConverter - preferences = i0.GeneratedColumn( - 'preferences', aliasedName, false, - type: i0.DriftSqlType.string, requiredDuringInsert: true) - .withConverter( - i1.$UserMetadataEntityTable.$converterpreferences); + late final i0.GeneratedColumnWithTypeConverter key = + i0.GeneratedColumn('key', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$UserMetadataEntityTable.$converterkey); @override - List get $columns => [userId, preferences]; + late final i0 + .GeneratedColumnWithTypeConverter, i3.Uint8List> + value = i0.GeneratedColumn('value', aliasedName, false, + type: i0.DriftSqlType.blob, requiredDuringInsert: true) + .withConverter>( + i1.$UserMetadataEntityTable.$convertervalue); + @override + List get $columns => [userId, key, value]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -311,7 +334,7 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity } @override - Set get $primaryKey => {userId}; + Set get $primaryKey => {userId, key}; @override i1.UserMetadataEntityData map(Map data, {String? tablePrefix}) { @@ -319,9 +342,12 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity return i1.UserMetadataEntityData( userId: attachedDatabase.typeMapping .read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!, - preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql( - attachedDatabase.typeMapping.read( - i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!), + key: i1.$UserMetadataEntityTable.$converterkey.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}key'])!), + value: i1.$UserMetadataEntityTable.$convertervalue.fromSql( + attachedDatabase.typeMapping + .read(i0.DriftSqlType.blob, data['${effectivePrefix}value'])!), ); } @@ -330,8 +356,11 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity return $UserMetadataEntityTable(attachedDatabase, alias); } - static i0.JsonTypeConverter2 - $converterpreferences = i3.userPreferenceConverter; + static i0.JsonTypeConverter2 $converterkey = + const i0.EnumIndexConverter( + i2.UserMetadataKey.values); + static i0.JsonTypeConverter2, i3.Uint8List, Object?> + $convertervalue = i4.userMetadataConverter; @override bool get withoutRowId => true; @override @@ -341,16 +370,21 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity class UserMetadataEntityData extends i0.DataClass implements i0.Insertable { final String userId; - final i2.UserPreferences preferences; + final i2.UserMetadataKey key; + final Map value; const UserMetadataEntityData( - {required this.userId, required this.preferences}); + {required this.userId, required this.key, required this.value}); @override Map toColumns(bool nullToAbsent) { final map = {}; map['user_id'] = i0.Variable(userId); { - map['preferences'] = i0.Variable( - i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences)); + map['key'] = i0.Variable( + i1.$UserMetadataEntityTable.$converterkey.toSql(key)); + } + { + map['value'] = i0.Variable( + i1.$UserMetadataEntityTable.$convertervalue.toSql(value)); } return map; } @@ -360,8 +394,10 @@ class UserMetadataEntityData extends i0.DataClass serializer ??= i0.driftRuntimeOptions.defaultSerializer; return UserMetadataEntityData( userId: serializer.fromJson(json['userId']), - preferences: i1.$UserMetadataEntityTable.$converterpreferences - .fromJson(serializer.fromJson(json['preferences'])), + key: i1.$UserMetadataEntityTable.$converterkey + .fromJson(serializer.fromJson(json['key'])), + value: i1.$UserMetadataEntityTable.$convertervalue + .fromJson(serializer.fromJson(json['value'])), ); } @override @@ -369,24 +405,28 @@ class UserMetadataEntityData extends i0.DataClass serializer ??= i0.driftRuntimeOptions.defaultSerializer; return { 'userId': serializer.toJson(userId), - 'preferences': serializer.toJson(i1 - .$UserMetadataEntityTable.$converterpreferences - .toJson(preferences)), + 'key': serializer + .toJson(i1.$UserMetadataEntityTable.$converterkey.toJson(key)), + 'value': serializer.toJson( + i1.$UserMetadataEntityTable.$convertervalue.toJson(value)), }; } i1.UserMetadataEntityData copyWith( - {String? userId, i2.UserPreferences? preferences}) => + {String? userId, + i2.UserMetadataKey? key, + Map? value}) => i1.UserMetadataEntityData( userId: userId ?? this.userId, - preferences: preferences ?? this.preferences, + key: key ?? this.key, + value: value ?? this.value, ); UserMetadataEntityData copyWithCompanion( i1.UserMetadataEntityCompanion data) { return UserMetadataEntityData( userId: data.userId.present ? data.userId.value : this.userId, - preferences: - data.preferences.present ? data.preferences.value : this.preferences, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, ); } @@ -394,49 +434,60 @@ class UserMetadataEntityData extends i0.DataClass String toString() { return (StringBuffer('UserMetadataEntityData(') ..write('userId: $userId, ') - ..write('preferences: $preferences') + ..write('key: $key, ') + ..write('value: $value') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(userId, preferences); + int get hashCode => Object.hash(userId, key, value); @override bool operator ==(Object other) => identical(this, other) || (other is i1.UserMetadataEntityData && other.userId == this.userId && - other.preferences == this.preferences); + other.key == this.key && + other.value == this.value); } class UserMetadataEntityCompanion extends i0.UpdateCompanion { final i0.Value userId; - final i0.Value preferences; + final i0.Value key; + final i0.Value> value; const UserMetadataEntityCompanion({ this.userId = const i0.Value.absent(), - this.preferences = const i0.Value.absent(), + this.key = const i0.Value.absent(), + this.value = const i0.Value.absent(), }); UserMetadataEntityCompanion.insert({ required String userId, - required i2.UserPreferences preferences, + required i2.UserMetadataKey key, + required Map value, }) : userId = i0.Value(userId), - preferences = i0.Value(preferences); + key = i0.Value(key), + value = i0.Value(value); static i0.Insertable custom({ i0.Expression? userId, - i0.Expression? preferences, + i0.Expression? key, + i0.Expression? value, }) { return i0.RawValuesInsertable({ if (userId != null) 'user_id': userId, - if (preferences != null) 'preferences': preferences, + if (key != null) 'key': key, + if (value != null) 'value': value, }); } i1.UserMetadataEntityCompanion copyWith( - {i0.Value? userId, i0.Value? preferences}) { + {i0.Value? userId, + i0.Value? key, + i0.Value>? value}) { return i1.UserMetadataEntityCompanion( userId: userId ?? this.userId, - preferences: preferences ?? this.preferences, + key: key ?? this.key, + value: value ?? this.value, ); } @@ -446,10 +497,13 @@ class UserMetadataEntityCompanion if (userId.present) { map['user_id'] = i0.Variable(userId.value); } - if (preferences.present) { - map['preferences'] = i0.Variable(i1 - .$UserMetadataEntityTable.$converterpreferences - .toSql(preferences.value)); + if (key.present) { + map['key'] = i0.Variable( + i1.$UserMetadataEntityTable.$converterkey.toSql(key.value)); + } + if (value.present) { + map['value'] = i0.Variable( + i1.$UserMetadataEntityTable.$convertervalue.toSql(value.value)); } return map; } @@ -458,7 +512,8 @@ class UserMetadataEntityCompanion String toString() { return (StringBuffer('UserMetadataEntityCompanion(') ..write('userId: $userId, ') - ..write('preferences: $preferences') + ..write('key: $key, ') + ..write('value: $value') ..write(')')) .toString(); } diff --git a/mobile/lib/infrastructure/repositories/api.repository.dart b/mobile/lib/infrastructure/repositories/api.repository.dart index 56c64c5512..15696c65b7 100644 --- a/mobile/lib/infrastructure/repositories/api.repository.dart +++ b/mobile/lib/infrastructure/repositories/api.repository.dart @@ -5,7 +5,7 @@ class ApiRepository { Future checkNull(Future future) async { final response = await future; - if (response == null) throw NoResponseDtoError(); + if (response == null) throw const NoResponseDtoError(); return response; } } diff --git a/mobile/lib/infrastructure/repositories/asset_face.repository.dart b/mobile/lib/infrastructure/repositories/asset_face.repository.dart new file mode 100644 index 0000000000..7b3b97058b --- /dev/null +++ b/mobile/lib/infrastructure/repositories/asset_face.repository.dart @@ -0,0 +1,30 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset_face.model.dart'; +import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftAssetFaceRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftAssetFaceRepository(this._db) : super(_db); + + Future> getAll() { + return _db.assetFaceEntity.select().map((assetFace) => assetFace.toDto()).get(); + } +} + +extension on AssetFaceEntityData { + AssetFace toDto() { + return AssetFace( + id: id, + assetId: assetId, + personId: personId, + imageWidth: imageWidth, + imageHeight: imageHeight, + boundingBoxX1: boundingBoxX1, + boundingBoxY1: boundingBoxY1, + boundingBoxX2: boundingBoxX2, + boundingBoxY2: boundingBoxY2, + sourceType: sourceType, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/asset_media.repository.dart b/mobile/lib/infrastructure/repositories/asset_media.repository.dart index d46c340028..e8bf9ace43 100644 --- a/mobile/lib/infrastructure/repositories/asset_media.repository.dart +++ b/mobile/lib/infrastructure/repositories/asset_media.repository.dart @@ -1,13 +1,11 @@ import 'dart:typed_data'; import 'dart:ui'; -import 'package:immich_mobile/domain/interfaces/asset_media.interface.dart'; import 'package:photo_manager/photo_manager.dart'; -class AssetMediaRepository implements IAssetMediaRepository { +class AssetMediaRepository { const AssetMediaRepository(); - @override Future getThumbnail( String id, { int quality = 80, diff --git a/mobile/lib/infrastructure/repositories/backup.repository.dart b/mobile/lib/infrastructure/repositories/backup.repository.dart new file mode 100644 index 0000000000..aaba90de5f --- /dev/null +++ b/mobile/lib/infrastructure/repositories/backup.repository.dart @@ -0,0 +1,149 @@ +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import "package:immich_mobile/utils/database.utils.dart"; + +final backupRepositoryProvider = Provider( + (ref) => DriftBackupRepository(ref.watch(driftProvider)), +); + +class DriftBackupRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftBackupRepository(this._db) : super(_db); + + _getExcludedSubquery() { + return _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), + useColumns: false, + ), + ]) + ..where( + _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.excluded), + ); + } + + Future getTotalCount() async { + final query = _db.localAlbumAssetEntity.selectOnly(distinct: true) + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), + useColumns: false, + ), + ]) + ..where( + _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) & + _db.localAlbumAssetEntity.assetId.isNotInQuery(_getExcludedSubquery()), + ); + + return query.get().then((rows) => rows.length); + } + + Future getRemainderCount(String userId) async { + final query = _db.localAlbumAssetEntity.selectOnly(distinct: true) + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), + useColumns: false, + ), + innerJoin( + _db.localAssetEntity, + _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), + useColumns: false, + ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum) & + _db.remoteAssetEntity.ownerId.equals(userId), + useColumns: false, + ), + ]) + ..where( + _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) & + _db.remoteAssetEntity.id.isNull() & + _db.localAlbumAssetEntity.assetId.isNotInQuery(_getExcludedSubquery()), + ); + + return query.get().then((rows) => rows.length); + } + + Future getBackupCount(String userId) async { + final query = _db.localAlbumAssetEntity.selectOnly(distinct: true) + ..addColumns( + [_db.localAlbumAssetEntity.assetId], + ) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), + useColumns: false, + ), + innerJoin( + _db.localAssetEntity, + _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), + useColumns: false, + ), + innerJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), + useColumns: false, + ), + ]) + ..where( + _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) & + _db.remoteAssetEntity.id.isNotNull() & + _db.remoteAssetEntity.ownerId.equals(userId) & + _db.localAlbumAssetEntity.assetId.isNotInQuery(_getExcludedSubquery()), + ); + + return query.get().then((rows) => rows.length); + } + + Future> getCandidates(String userId) async { + final selectedAlbumIds = _db.localAlbumEntity.selectOnly(distinct: true) + ..addColumns([_db.localAlbumEntity.id]) + ..where( + _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected), + ); + + final query = _db.localAssetEntity.select() + ..where( + (lae) => + existsQuery( + _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..where( + _db.localAlbumAssetEntity.albumId.isInQuery(selectedAlbumIds) & + _db.localAlbumAssetEntity.assetId.equalsExp(lae.id), + ), + ) & + notExistsQuery( + _db.remoteAssetEntity.selectOnly() + ..addColumns([_db.remoteAssetEntity.checksum]) + ..where( + _db.remoteAssetEntity.checksum.equalsExp(lae.checksum) & + _db.remoteAssetEntity.ownerId.equals(userId) & + lae.checksum.isNotNull(), + ), + ) & + lae.id.isNotInQuery(_getExcludedSubquery()), + ) + ..orderBy( + [ + (localAsset) => OrderingTerm.desc(localAsset.createdAt), + ], + ); + + return query.map((localAsset) => localAsset.toDto()).get(); + } +} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 15b19f5c80..15fce4d649 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -2,15 +2,25 @@ 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_face.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/person.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/stack.entity.dart'; 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.steps.dart'; import 'package:isar/isar.dart'; import 'db.repository.drift.dart'; @@ -40,6 +50,14 @@ class IsarDatabaseRepository implements IDatabaseRepository { LocalAlbumAssetEntity, RemoteAssetEntity, RemoteExifEntity, + RemoteAlbumEntity, + RemoteAlbumAssetEntity, + RemoteAlbumUserEntity, + MemoryEntity, + MemoryAssetEntity, + StackEntity, + PersonEntity, + AssetFaceEntity, ], include: { 'package:immich_mobile/infrastructure/entities/merged_asset.drift', @@ -56,10 +74,45 @@ class Drift extends $Drift implements IDatabaseRepository { ); @override - int get schemaVersion => 1; + int get schemaVersion => 4; @override MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (m, from, to) async { + // Run migration steps without foreign keys and re-enable them later + await customStatement('PRAGMA foreign_keys = OFF'); + + await m.runMigrationSteps( + from: from, + to: to, + steps: migrationSteps( + from1To2: (m, v2) async { + for (final entity in v2.entities) { + await m.drop(entity); + await m.create(entity); + } + }, + from2To3: (m, v3) async { + // Removed foreign key constraint on stack.primaryAssetId + await m.alterTable(TableMigration(v3.stackEntity)); + }, + from3To4: (m, v4) async { + // Thumbnail path column got removed from person_entity + await m.alterTable(TableMigration(v4.personEntity)); + // asset_face_entity is added + await m.create(v4.assetFaceEntity); + }, + ), + ); + + if (kDebugMode) { + // Fail if the migration broke foreign keys + final wrongFKs = await customSelect('PRAGMA foreign_key_check').get(); + assert(wrongFKs.isEmpty, '${wrongFKs.map((e) => e.data)}'); + } + + await customStatement('PRAGMA foreign_keys = ON;'); + }, beforeOpen: (details) async { await customStatement('PRAGMA foreign_keys = ON'); await customStatement('PRAGMA synchronous = NORMAL'); @@ -73,6 +126,5 @@ class DriftDatabaseRepository implements IDatabaseRepository { const DriftDatabaseRepository(this._db); @override - Future transaction(Future Function() callback) => - _db.transaction(callback); + Future transaction(Future Function() callback) => _db.transaction(callback); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index d088e5420a..f5962f09ab 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -5,21 +5,37 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' as i1; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' as i2; -import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart' as i3; -import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' as i4; -import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' - as i5; import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' - as i6; + as i5; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' + as i6; +import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' as i7; -import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' as i8; -import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' +import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' as i9; -import 'package:drift/internal/modular.dart' as i10; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' + as i10; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart' + as i11; +import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart' + as i12; +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' + as i13; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' + as i14; +import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' + as i15; +import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart' + as i16; +import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' + as i17; +import 'package:drift/internal/modular.dart' as i18; abstract class $Drift extends i0.GeneratedDatabase { $Drift(i0.QueryExecutor e) : super(e); @@ -27,20 +43,33 @@ abstract class $Drift extends i0.GeneratedDatabase { late final i1.$UserEntityTable userEntity = i1.$UserEntityTable(this); late final i2.$RemoteAssetEntityTable remoteAssetEntity = i2.$RemoteAssetEntityTable(this); - late final i3.$LocalAssetEntityTable localAssetEntity = - i3.$LocalAssetEntityTable(this); - late final i4.$UserMetadataEntityTable userMetadataEntity = - i4.$UserMetadataEntityTable(this); - late final i5.$PartnerEntityTable partnerEntity = - i5.$PartnerEntityTable(this); - late final i6.$LocalAlbumEntityTable localAlbumEntity = - i6.$LocalAlbumEntityTable(this); - late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity = - i7.$LocalAlbumAssetEntityTable(this); - late final i8.$RemoteExifEntityTable remoteExifEntity = - i8.$RemoteExifEntityTable(this); - i9.MergedAssetDrift get mergedAssetDrift => i10.ReadDatabaseContainer(this) - .accessor(i9.MergedAssetDrift.new); + late final i3.$StackEntityTable stackEntity = i3.$StackEntityTable(this); + late final i4.$LocalAssetEntityTable localAssetEntity = + i4.$LocalAssetEntityTable(this); + late final i5.$LocalAlbumEntityTable localAlbumEntity = + i5.$LocalAlbumEntityTable(this); + late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity = + i6.$LocalAlbumAssetEntityTable(this); + late final i7.$UserMetadataEntityTable userMetadataEntity = + i7.$UserMetadataEntityTable(this); + late final i8.$PartnerEntityTable partnerEntity = + i8.$PartnerEntityTable(this); + late final i9.$RemoteExifEntityTable remoteExifEntity = + i9.$RemoteExifEntityTable(this); + late final i10.$RemoteAlbumEntityTable remoteAlbumEntity = + i10.$RemoteAlbumEntityTable(this); + late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = + i11.$RemoteAlbumAssetEntityTable(this); + late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = + i12.$RemoteAlbumUserEntityTable(this); + late final i13.$MemoryEntityTable memoryEntity = i13.$MemoryEntityTable(this); + late final i14.$MemoryAssetEntityTable memoryAssetEntity = + i14.$MemoryAssetEntityTable(this); + late final i15.$PersonEntityTable personEntity = i15.$PersonEntityTable(this); + late final i16.$AssetFaceEntityTable assetFaceEntity = + i16.$AssetFaceEntityTable(this); + i17.MergedAssetDrift get mergedAssetDrift => i18.ReadDatabaseContainer(this) + .accessor(i17.MergedAssetDrift.new); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -48,15 +77,23 @@ abstract class $Drift extends i0.GeneratedDatabase { List get allSchemaEntities => [ userEntity, remoteAssetEntity, + stackEntity, localAssetEntity, - i3.idxLocalAssetChecksum, + localAlbumEntity, + localAlbumAssetEntity, + i4.idxLocalAssetChecksum, i2.uQRemoteAssetOwnerChecksum, i2.idxRemoteAssetChecksum, userMetadataEntity, partnerEntity, - localAlbumEntity, - localAlbumAssetEntity, - remoteExifEntity + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity ]; @override i0.StreamQueryUpdateRules get streamUpdateRules => @@ -69,6 +106,29 @@ abstract class $Drift extends i0.GeneratedDatabase { i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete), ], ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('local_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('local_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('local_album_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('local_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), i0.WritePropagation( on: i0.TableUpdateQuery.onTableName('user_entity', limitUpdateKind: i0.UpdateKind.delete), @@ -92,26 +152,98 @@ abstract class $Drift extends i0.GeneratedDatabase { ], ), i0.WritePropagation( - on: i0.TableUpdateQuery.onTableName('local_asset_entity', + on: i0.TableUpdateQuery.onTableName('remote_asset_entity', limitUpdateKind: i0.UpdateKind.delete), result: [ - i0.TableUpdate('local_album_asset_entity', - kind: i0.UpdateKind.delete), + i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete), ], ), i0.WritePropagation( - on: i0.TableUpdateQuery.onTableName('local_album_entity', + on: i0.TableUpdateQuery.onTableName('user_entity', limitUpdateKind: i0.UpdateKind.delete), result: [ - i0.TableUpdate('local_album_asset_entity', - kind: i0.UpdateKind.delete), + i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete), ], ), i0.WritePropagation( on: i0.TableUpdateQuery.onTableName('remote_asset_entity', limitUpdateKind: i0.UpdateKind.delete), result: [ - i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete), + i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.update), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_album_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_album_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_album_user_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_album_user_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('memory_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('memory_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('person_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('asset_face_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('person_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('asset_face_entity', kind: i0.UpdateKind.update), ], ), ], @@ -128,16 +260,33 @@ class $DriftManager { i1.$$UserEntityTableTableManager(_db, _db.userEntity); i2.$$RemoteAssetEntityTableTableManager get remoteAssetEntity => i2.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity); - i3.$$LocalAssetEntityTableTableManager get localAssetEntity => - i3.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); - i4.$$UserMetadataEntityTableTableManager get userMetadataEntity => - i4.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); - i5.$$PartnerEntityTableTableManager get partnerEntity => - i5.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); - i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity => - i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); - i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7 + i3.$$StackEntityTableTableManager get stackEntity => + i3.$$StackEntityTableTableManager(_db, _db.stackEntity); + i4.$$LocalAssetEntityTableTableManager get localAssetEntity => + i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); + i5.$$LocalAlbumEntityTableTableManager get localAlbumEntity => + i5.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); + i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6 .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); - i8.$$RemoteExifEntityTableTableManager get remoteExifEntity => - i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); + i7.$$UserMetadataEntityTableTableManager get userMetadataEntity => + i7.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); + i8.$$PartnerEntityTableTableManager get partnerEntity => + i8.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); + i9.$$RemoteExifEntityTableTableManager get remoteExifEntity => + i9.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); + i10.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity => + i10.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity); + i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity => + i11.$$RemoteAlbumAssetEntityTableTableManager( + _db, _db.remoteAlbumAssetEntity); + i12.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i12 + .$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity); + i13.$$MemoryEntityTableTableManager get memoryEntity => + i13.$$MemoryEntityTableTableManager(_db, _db.memoryEntity); + i14.$$MemoryAssetEntityTableTableManager get memoryAssetEntity => + i14.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); + i15.$$PersonEntityTableTableManager get personEntity => + i15.$$PersonEntityTableTableManager(_db, _db.personEntity); + i16.$$AssetFaceEntityTableTableManager get assetFaceEntity => + i16.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart new file mode 100644 index 0000000000..57c90f731d --- /dev/null +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -0,0 +1,1516 @@ +// dart format width=80 +import 'package:drift/internal/versioned_schema.dart' as i0; +import 'package:drift/drift.dart' as i1; +import 'dart:typed_data' as i2; +import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import + +// GENERATED BY drift_dev, DO NOT MODIFY. +final class Schema2 extends i0.VersionedSchema { + Schema2({required super.database}) : super(version: 2); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + localAssetEntity, + stackEntity, + idxLocalAssetChecksum, + uQRemoteAssetOwnerChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAlbumAssetEntity, + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + ]; + late final Shape0 userEntity = Shape0( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 remoteAssetEntity = Shape1( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape2 localAssetEntity = Shape2( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_22, + _column_14, + _column_23, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_24, + ], + attachedDatabase: database, + ), + alias: null); + final i1.Index idxLocalAssetChecksum = + i1.Index('idx_local_asset_checksum', 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + final i1.Index uQRemoteAssetOwnerChecksum = i1.Index('UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + final i1.Index idxRemoteAssetChecksum = + i1.Index('idx_remote_asset_checksum', 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(user_id, "key")', + ], + columns: [ + _column_25, + _column_26, + _column_27, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(shared_by_id, shared_with_id)', + ], + columns: [ + _column_28, + _column_29, + _column_30, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape6 localAlbumEntity = Shape6( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_33, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape7 localAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, album_id)', + ], + columns: [ + _column_34, + _column_35, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id)', + ], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, album_id)', + ], + columns: [ + _column_36, + _column_60, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(album_id, user_id)', + ], + columns: [ + _column_60, + _column_25, + _column_61, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, memory_id)', + ], + columns: [ + _column_36, + _column_68, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape13 personEntity = Shape13( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_70, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape0 extends i0.VersionedTable { + Shape0({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get isAdmin => columnsByName['is_admin']! as i1.GeneratedColumn; + i1.GeneratedColumn get email => columnsByName['email']! as i1.GeneratedColumn; + i1.GeneratedColumn get profileImagePath => columnsByName['profile_image_path']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get quotaSizeInBytes => columnsByName['quota_size_in_bytes']! as i1.GeneratedColumn; + i1.GeneratedColumn get quotaUsageInBytes => columnsByName['quota_usage_in_bytes']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_0(String aliasedName) => + i1.GeneratedColumn('id', aliasedName, false, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_1(String aliasedName) => + i1.GeneratedColumn('name', aliasedName, false, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_2(String aliasedName) => i1.GeneratedColumn('is_admin', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'), + defaultValue: const CustomExpression('0')); +i1.GeneratedColumn _column_3(String aliasedName) => + i1.GeneratedColumn('email', aliasedName, false, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_4(String aliasedName) => + i1.GeneratedColumn('profile_image_path', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_5(String aliasedName) => + i1.GeneratedColumn('updated_at', aliasedName, false, + type: i1.DriftSqlType.dateTime, defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); +i1.GeneratedColumn _column_6(String aliasedName) => + i1.GeneratedColumn('quota_size_in_bytes', aliasedName, true, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_7(String aliasedName) => + i1.GeneratedColumn('quota_usage_in_bytes', aliasedName, false, + type: i1.DriftSqlType.int, defaultValue: const CustomExpression('0')); + +class Shape1 extends i0.VersionedTable { + Shape1({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get name => columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get width => columnsByName['width']! as i1.GeneratedColumn; + i1.GeneratedColumn get height => columnsByName['height']! as i1.GeneratedColumn; + i1.GeneratedColumn get durationInSeconds => columnsByName['duration_in_seconds']! as i1.GeneratedColumn; + i1.GeneratedColumn get id => columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get checksum => columnsByName['checksum']! as i1.GeneratedColumn; + i1.GeneratedColumn get isFavorite => columnsByName['is_favorite']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get localDateTime => columnsByName['local_date_time']! as i1.GeneratedColumn; + i1.GeneratedColumn get thumbHash => columnsByName['thumb_hash']! as i1.GeneratedColumn; + i1.GeneratedColumn get deletedAt => columnsByName['deleted_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get livePhotoVideoId => + columnsByName['live_photo_video_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get visibility => columnsByName['visibility']! as i1.GeneratedColumn; + i1.GeneratedColumn get stackId => columnsByName['stack_id']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_8(String aliasedName) => + i1.GeneratedColumn('type', aliasedName, false, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_9(String aliasedName) => + i1.GeneratedColumn('created_at', aliasedName, false, + type: i1.DriftSqlType.dateTime, defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); +i1.GeneratedColumn _column_10(String aliasedName) => + i1.GeneratedColumn('width', aliasedName, true, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_11(String aliasedName) => + i1.GeneratedColumn('height', aliasedName, true, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_12(String aliasedName) => + i1.GeneratedColumn('duration_in_seconds', aliasedName, true, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_13(String aliasedName) => + i1.GeneratedColumn('checksum', aliasedName, false, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_14(String aliasedName) => i1.GeneratedColumn('is_favorite', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); +i1.GeneratedColumn _column_15(String aliasedName) => i1.GeneratedColumn('owner_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); +i1.GeneratedColumn _column_16(String aliasedName) => + i1.GeneratedColumn('local_date_time', aliasedName, true, type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_17(String aliasedName) => + i1.GeneratedColumn('thumb_hash', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_18(String aliasedName) => + i1.GeneratedColumn('deleted_at', aliasedName, true, type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_19(String aliasedName) => + i1.GeneratedColumn('live_photo_video_id', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_20(String aliasedName) => + i1.GeneratedColumn('visibility', aliasedName, false, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_21(String aliasedName) => + i1.GeneratedColumn('stack_id', aliasedName, true, type: i1.DriftSqlType.string); + +class Shape2 extends i0.VersionedTable { + Shape2({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get name => columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get width => columnsByName['width']! as i1.GeneratedColumn; + i1.GeneratedColumn get height => columnsByName['height']! as i1.GeneratedColumn; + i1.GeneratedColumn get durationInSeconds => columnsByName['duration_in_seconds']! as i1.GeneratedColumn; + i1.GeneratedColumn get id => columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get checksum => columnsByName['checksum']! as i1.GeneratedColumn; + i1.GeneratedColumn get isFavorite => columnsByName['is_favorite']! as i1.GeneratedColumn; + i1.GeneratedColumn get orientation => columnsByName['orientation']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_22(String aliasedName) => + i1.GeneratedColumn('checksum', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_23(String aliasedName) => i1.GeneratedColumn('orientation', aliasedName, false, + type: i1.DriftSqlType.int, defaultValue: const CustomExpression('0')); + +class Shape3 extends i0.VersionedTable { + Shape3({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get primaryAssetId => columnsByName['primary_asset_id']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_24(String aliasedName) => + i1.GeneratedColumn('primary_asset_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id)')); + +class Shape4 extends i0.VersionedTable { + Shape4({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get userId => columnsByName['user_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get key => columnsByName['key']! as i1.GeneratedColumn; + i1.GeneratedColumn get value => columnsByName['value']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_25(String aliasedName) => i1.GeneratedColumn('user_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); +i1.GeneratedColumn _column_26(String aliasedName) => + i1.GeneratedColumn('key', aliasedName, false, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_27(String aliasedName) => + i1.GeneratedColumn('value', aliasedName, false, type: i1.DriftSqlType.blob); + +class Shape5 extends i0.VersionedTable { + Shape5({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get sharedById => columnsByName['shared_by_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get sharedWithId => columnsByName['shared_with_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get inTimeline => columnsByName['in_timeline']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_28(String aliasedName) => + i1.GeneratedColumn('shared_by_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); +i1.GeneratedColumn _column_29(String aliasedName) => + i1.GeneratedColumn('shared_with_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); +i1.GeneratedColumn _column_30(String aliasedName) => i1.GeneratedColumn('in_timeline', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('CHECK ("in_timeline" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + +class Shape6 extends i0.VersionedTable { + Shape6({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get backupSelection => columnsByName['backup_selection']! as i1.GeneratedColumn; + i1.GeneratedColumn get isIosSharedAlbum => columnsByName['is_ios_shared_album']! as i1.GeneratedColumn; + i1.GeneratedColumn get marker_ => columnsByName['marker']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_31(String aliasedName) => + i1.GeneratedColumn('backup_selection', aliasedName, false, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_32(String aliasedName) => + i1.GeneratedColumn('is_ios_shared_album', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('CHECK ("is_ios_shared_album" IN (0, 1))'), + defaultValue: const CustomExpression('0')); +i1.GeneratedColumn _column_33(String aliasedName) => i1.GeneratedColumn('marker', aliasedName, true, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))')); + +class Shape7 extends i0.VersionedTable { + Shape7({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get assetId => columnsByName['asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get albumId => columnsByName['album_id']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_34(String aliasedName) => i1.GeneratedColumn('asset_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('REFERENCES local_asset_entity (id) ON DELETE CASCADE')); +i1.GeneratedColumn _column_35(String aliasedName) => i1.GeneratedColumn('album_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('REFERENCES local_album_entity (id) ON DELETE CASCADE')); + +class Shape8 extends i0.VersionedTable { + Shape8({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get assetId => columnsByName['asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get city => columnsByName['city']! as i1.GeneratedColumn; + i1.GeneratedColumn get state => columnsByName['state']! as i1.GeneratedColumn; + i1.GeneratedColumn get country => columnsByName['country']! as i1.GeneratedColumn; + i1.GeneratedColumn get dateTimeOriginal => + columnsByName['date_time_original']! as i1.GeneratedColumn; + i1.GeneratedColumn get description => columnsByName['description']! as i1.GeneratedColumn; + i1.GeneratedColumn get height => columnsByName['height']! as i1.GeneratedColumn; + i1.GeneratedColumn get width => columnsByName['width']! as i1.GeneratedColumn; + i1.GeneratedColumn get exposureTime => columnsByName['exposure_time']! as i1.GeneratedColumn; + i1.GeneratedColumn get fNumber => columnsByName['f_number']! as i1.GeneratedColumn; + i1.GeneratedColumn get fileSize => columnsByName['file_size']! as i1.GeneratedColumn; + i1.GeneratedColumn get focalLength => columnsByName['focal_length']! as i1.GeneratedColumn; + i1.GeneratedColumn get latitude => columnsByName['latitude']! as i1.GeneratedColumn; + i1.GeneratedColumn get longitude => columnsByName['longitude']! as i1.GeneratedColumn; + i1.GeneratedColumn get iso => columnsByName['iso']! as i1.GeneratedColumn; + i1.GeneratedColumn get make => columnsByName['make']! as i1.GeneratedColumn; + i1.GeneratedColumn get model => columnsByName['model']! as i1.GeneratedColumn; + i1.GeneratedColumn get lens => columnsByName['lens']! as i1.GeneratedColumn; + i1.GeneratedColumn get orientation => columnsByName['orientation']! as i1.GeneratedColumn; + i1.GeneratedColumn get timeZone => columnsByName['time_zone']! as i1.GeneratedColumn; + i1.GeneratedColumn get rating => columnsByName['rating']! as i1.GeneratedColumn; + i1.GeneratedColumn get projectionType => columnsByName['projection_type']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_36(String aliasedName) => i1.GeneratedColumn('asset_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); +i1.GeneratedColumn _column_37(String aliasedName) => + i1.GeneratedColumn('city', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_38(String aliasedName) => + i1.GeneratedColumn('state', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_39(String aliasedName) => + i1.GeneratedColumn('country', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_40(String aliasedName) => + i1.GeneratedColumn('date_time_original', aliasedName, true, type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_41(String aliasedName) => + i1.GeneratedColumn('description', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_42(String aliasedName) => + i1.GeneratedColumn('exposure_time', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_43(String aliasedName) => + i1.GeneratedColumn('f_number', aliasedName, true, type: i1.DriftSqlType.double); +i1.GeneratedColumn _column_44(String aliasedName) => + i1.GeneratedColumn('file_size', aliasedName, true, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_45(String aliasedName) => + i1.GeneratedColumn('focal_length', aliasedName, true, type: i1.DriftSqlType.double); +i1.GeneratedColumn _column_46(String aliasedName) => + i1.GeneratedColumn('latitude', aliasedName, true, type: i1.DriftSqlType.double); +i1.GeneratedColumn _column_47(String aliasedName) => + i1.GeneratedColumn('longitude', aliasedName, true, type: i1.DriftSqlType.double); +i1.GeneratedColumn _column_48(String aliasedName) => + i1.GeneratedColumn('iso', aliasedName, true, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_49(String aliasedName) => + i1.GeneratedColumn('make', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_50(String aliasedName) => + i1.GeneratedColumn('model', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_51(String aliasedName) => + i1.GeneratedColumn('lens', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_52(String aliasedName) => + i1.GeneratedColumn('orientation', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_53(String aliasedName) => + i1.GeneratedColumn('time_zone', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_54(String aliasedName) => + i1.GeneratedColumn('rating', aliasedName, true, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_55(String aliasedName) => + i1.GeneratedColumn('projection_type', aliasedName, true, type: i1.DriftSqlType.string); + +class Shape9 extends i0.VersionedTable { + Shape9({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get description => columnsByName['description']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get thumbnailAssetId => columnsByName['thumbnail_asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get isActivityEnabled => columnsByName['is_activity_enabled']! as i1.GeneratedColumn; + i1.GeneratedColumn get order => columnsByName['order']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_56(String aliasedName) => + i1.GeneratedColumn('description', aliasedName, false, + type: i1.DriftSqlType.string, defaultValue: const CustomExpression('\'\'')); +i1.GeneratedColumn _column_57(String aliasedName) => + i1.GeneratedColumn('thumbnail_asset_id', aliasedName, true, + type: i1.DriftSqlType.string, + defaultConstraints: + i1.GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE SET NULL')); +i1.GeneratedColumn _column_58(String aliasedName) => + i1.GeneratedColumn('is_activity_enabled', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('CHECK ("is_activity_enabled" IN (0, 1))'), + defaultValue: const CustomExpression('1')); +i1.GeneratedColumn _column_59(String aliasedName) => + i1.GeneratedColumn('order', aliasedName, false, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_60(String aliasedName) => i1.GeneratedColumn('album_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + +class Shape10 extends i0.VersionedTable { + Shape10({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get albumId => columnsByName['album_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get userId => columnsByName['user_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get role => columnsByName['role']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_61(String aliasedName) => + i1.GeneratedColumn('role', aliasedName, false, type: i1.DriftSqlType.int); + +class Shape11 extends i0.VersionedTable { + Shape11({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get deletedAt => columnsByName['deleted_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get data => columnsByName['data']! as i1.GeneratedColumn; + i1.GeneratedColumn get isSaved => columnsByName['is_saved']! as i1.GeneratedColumn; + i1.GeneratedColumn get memoryAt => columnsByName['memory_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get seenAt => columnsByName['seen_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get showAt => columnsByName['show_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get hideAt => columnsByName['hide_at']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_62(String aliasedName) => + i1.GeneratedColumn('data', aliasedName, false, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_63(String aliasedName) => i1.GeneratedColumn('is_saved', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('CHECK ("is_saved" IN (0, 1))'), + defaultValue: const CustomExpression('0')); +i1.GeneratedColumn _column_64(String aliasedName) => + i1.GeneratedColumn('memory_at', aliasedName, false, type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_65(String aliasedName) => + i1.GeneratedColumn('seen_at', aliasedName, true, type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_66(String aliasedName) => + i1.GeneratedColumn('show_at', aliasedName, true, type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_67(String aliasedName) => + i1.GeneratedColumn('hide_at', aliasedName, true, type: i1.DriftSqlType.dateTime); + +class Shape12 extends i0.VersionedTable { + Shape12({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get assetId => columnsByName['asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get memoryId => columnsByName['memory_id']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_68(String aliasedName) => i1.GeneratedColumn('memory_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('REFERENCES memory_entity (id) ON DELETE CASCADE')); + +class Shape13 extends i0.VersionedTable { + Shape13({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get faceAssetId => columnsByName['face_asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get thumbnailPath => columnsByName['thumbnail_path']! as i1.GeneratedColumn; + i1.GeneratedColumn get isFavorite => columnsByName['is_favorite']! as i1.GeneratedColumn; + i1.GeneratedColumn get isHidden => columnsByName['is_hidden']! as i1.GeneratedColumn; + i1.GeneratedColumn get color => columnsByName['color']! as i1.GeneratedColumn; + i1.GeneratedColumn get birthDate => columnsByName['birth_date']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_69(String aliasedName) => + i1.GeneratedColumn('face_asset_id', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_70(String aliasedName) => + i1.GeneratedColumn('thumbnail_path', aliasedName, false, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_71(String aliasedName) => i1.GeneratedColumn('is_favorite', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))')); +i1.GeneratedColumn _column_72(String aliasedName) => i1.GeneratedColumn('is_hidden', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('CHECK ("is_hidden" IN (0, 1))')); +i1.GeneratedColumn _column_73(String aliasedName) => + i1.GeneratedColumn('color', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_74(String aliasedName) => + i1.GeneratedColumn('birth_date', aliasedName, true, type: i1.DriftSqlType.dateTime); + +final class Schema3 extends i0.VersionedSchema { + Schema3({required super.database}) : super(version: 3); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + localAssetEntity, + stackEntity, + idxLocalAssetChecksum, + uQRemoteAssetOwnerChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAlbumAssetEntity, + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + ]; + late final Shape0 userEntity = Shape0( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 remoteAssetEntity = Shape1( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape2 localAssetEntity = Shape2( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_22, + _column_14, + _column_23, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_75, + ], + attachedDatabase: database, + ), + alias: null); + final i1.Index idxLocalAssetChecksum = + i1.Index('idx_local_asset_checksum', 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + final i1.Index uQRemoteAssetOwnerChecksum = i1.Index('UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + final i1.Index idxRemoteAssetChecksum = + i1.Index('idx_remote_asset_checksum', 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(user_id, "key")', + ], + columns: [ + _column_25, + _column_26, + _column_27, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(shared_by_id, shared_with_id)', + ], + columns: [ + _column_28, + _column_29, + _column_30, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape6 localAlbumEntity = Shape6( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_33, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape7 localAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, album_id)', + ], + columns: [ + _column_34, + _column_35, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id)', + ], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, album_id)', + ], + columns: [ + _column_36, + _column_60, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(album_id, user_id)', + ], + columns: [ + _column_60, + _column_25, + _column_61, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, memory_id)', + ], + columns: [ + _column_36, + _column_68, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape13 personEntity = Shape13( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_70, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null); +} + +i1.GeneratedColumn _column_75(String aliasedName) => + i1.GeneratedColumn('primary_asset_id', aliasedName, false, type: i1.DriftSqlType.string); + +final class Schema4 extends i0.VersionedSchema { + Schema4({required super.database}) : super(version: 4); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + uQRemoteAssetOwnerChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + ]; + late final Shape0 userEntity = Shape0( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 remoteAssetEntity = Shape1( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_75, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape2 localAssetEntity = Shape2( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_22, + _column_14, + _column_23, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape6 localAlbumEntity = Shape6( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_33, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape7 localAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, album_id)', + ], + columns: [ + _column_34, + _column_35, + ], + attachedDatabase: database, + ), + alias: null); + final i1.Index idxLocalAssetChecksum = + i1.Index('idx_local_asset_checksum', 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + final i1.Index uQRemoteAssetOwnerChecksum = i1.Index('UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + final i1.Index idxRemoteAssetChecksum = + i1.Index('idx_remote_asset_checksum', 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(user_id, "key")', + ], + columns: [ + _column_25, + _column_26, + _column_27, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(shared_by_id, shared_with_id)', + ], + columns: [ + _column_28, + _column_29, + _column_30, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id)', + ], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, album_id)', + ], + columns: [ + _column_36, + _column_60, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(album_id, user_id)', + ], + columns: [ + _column_60, + _column_25, + _column_61, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, memory_id)', + ], + columns: [ + _column_36, + _column_68, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape15 assetFaceEntity = Shape15( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape14 extends i0.VersionedTable { + Shape14({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get faceAssetId => columnsByName['face_asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get isFavorite => columnsByName['is_favorite']! as i1.GeneratedColumn; + i1.GeneratedColumn get isHidden => columnsByName['is_hidden']! as i1.GeneratedColumn; + i1.GeneratedColumn get color => columnsByName['color']! as i1.GeneratedColumn; + i1.GeneratedColumn get birthDate => columnsByName['birth_date']! as i1.GeneratedColumn; +} + +class Shape15 extends i0.VersionedTable { + Shape15({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get assetId => columnsByName['asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get personId => columnsByName['person_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get imageWidth => columnsByName['image_width']! as i1.GeneratedColumn; + i1.GeneratedColumn get imageHeight => columnsByName['image_height']! as i1.GeneratedColumn; + i1.GeneratedColumn get boundingBoxX1 => columnsByName['bounding_box_x1']! as i1.GeneratedColumn; + i1.GeneratedColumn get boundingBoxY1 => columnsByName['bounding_box_y1']! as i1.GeneratedColumn; + i1.GeneratedColumn get boundingBoxX2 => columnsByName['bounding_box_x2']! as i1.GeneratedColumn; + i1.GeneratedColumn get boundingBoxY2 => columnsByName['bounding_box_y2']! as i1.GeneratedColumn; + i1.GeneratedColumn get sourceType => columnsByName['source_type']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_76(String aliasedName) => i1.GeneratedColumn('person_id', aliasedName, true, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways('REFERENCES person_entity (id) ON DELETE SET NULL')); +i1.GeneratedColumn _column_77(String aliasedName) => + i1.GeneratedColumn('image_width', aliasedName, false, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_78(String aliasedName) => + i1.GeneratedColumn('image_height', aliasedName, false, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_79(String aliasedName) => + i1.GeneratedColumn('bounding_box_x1', aliasedName, false, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_80(String aliasedName) => + i1.GeneratedColumn('bounding_box_y1', aliasedName, false, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_81(String aliasedName) => + i1.GeneratedColumn('bounding_box_x2', aliasedName, false, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_82(String aliasedName) => + i1.GeneratedColumn('bounding_box_y2', aliasedName, false, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_83(String aliasedName) => + i1.GeneratedColumn('source_type', aliasedName, false, type: i1.DriftSqlType.string); +i0.MigrationStepWithVersion migrationSteps({ + required Future Function(i1.Migrator m, Schema2 schema) from1To2, + required Future Function(i1.Migrator m, Schema3 schema) from2To3, + required Future Function(i1.Migrator m, Schema4 schema) from3To4, +}) { + return (currentVersion, database) async { + switch (currentVersion) { + case 1: + final schema = Schema2(database: database); + final migrator = i1.Migrator(database, schema); + await from1To2(migrator, schema); + return 2; + case 2: + final schema = Schema3(database: database); + final migrator = i1.Migrator(database, schema); + await from2To3(migrator, schema); + return 3; + case 3: + final schema = Schema4(database: database); + final migrator = i1.Migrator(database, schema); + await from3To4(migrator, schema); + return 4; + default: + throw ArgumentError.value('Unknown migration from $currentVersion'); + } + }; +} + +i1.OnUpgrade stepByStep({ + required Future Function(i1.Migrator m, Schema2 schema) from1To2, + required Future Function(i1.Migrator m, Schema3 schema) from2To3, + required Future Function(i1.Migrator m, Schema4 schema) from3To4, +}) => + i0.VersionedSchema.stepByStepHelper( + step: migrationSteps( + from1To2: from1To2, + from2To3: from2To3, + from3To4: from3To4, + )); diff --git a/mobile/lib/infrastructure/repositories/device_asset.repository.dart b/mobile/lib/infrastructure/repositories/device_asset.repository.dart index 87784ecaab..73ee148ab3 100644 --- a/mobile/lib/infrastructure/repositories/device_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/device_asset.repository.dart @@ -1,23 +1,19 @@ -import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart'; 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 - implements IDeviceAssetRepository { +class IsarDeviceAssetRepository extends IsarDatabaseRepository { final Isar _db; const IsarDeviceAssetRepository(this._db) : super(_db); - @override Future deleteIds(List ids) { return transaction(() async { await _db.deviceAssetEntitys.deleteAllByAssetId(ids.toList()); }); } - @override Future> getByIds(List localIds) { return _db.deviceAssetEntitys .where() @@ -26,11 +22,9 @@ class IsarDeviceAssetRepository extends IsarDatabaseRepository .then((value) => value.map((e) => e.toModel()).toList()); } - @override Future updateAll(List assetHash) { return transaction(() async { - await _db.deviceAssetEntitys - .putAll(assetHash.map(DeviceAssetEntity.fromDto).toList()); + 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 index 2b4276dd57..726e51b77d 100644 --- a/mobile/lib/infrastructure/repositories/exif.repository.dart +++ b/mobile/lib/infrastructure/repositories/exif.repository.dart @@ -1,36 +1,29 @@ -import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; 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/entities/exif.entity.dart' as entity; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:isar/isar.dart'; -class IsarExifRepository extends IsarDatabaseRepository - implements IExifInfoRepository { +class IsarExifRepository extends IsarDatabaseRepository { final Isar _db; const IsarExifRepository(this._db) : super(_db); - @override Future delete(int assetId) async { await transaction(() async { await _db.exifInfos.delete(assetId); }); } - @override Future deleteAll() async { await transaction(() async { await _db.exifInfos.clear(); }); } - @override Future get(int assetId) async { return (await _db.exifInfos.get(assetId))?.toDto(); } - @override Future update(ExifInfo exifInfo) { return transaction(() async { await _db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo)); @@ -38,7 +31,6 @@ class IsarExifRepository extends IsarDatabaseRepository }); } - @override Future> updateAll(List exifInfos) { return transaction(() async { await _db.exifInfos.putAll( diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index 4f46b9b408..c1e5724b4c 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -1,22 +1,22 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/interfaces/local_album.interface.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/local_album.model.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/utils/database.utils.dart'; import 'package:platform/platform.dart'; -class DriftLocalAlbumRepository extends DriftDatabaseRepository - implements ILocalAlbumRepository { +enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount } + +class DriftLocalAlbumRepository extends DriftDatabaseRepository { final Drift _db; final Platform _platform; const DriftLocalAlbumRepository(this._db, {Platform? platform}) : _platform = platform ?? const LocalPlatform(), super(_db); - @override Future> getAll({Set sortBy = const {}}) { final assetCount = _db.localAlbumAssetEntity.assetId.count(); @@ -37,10 +37,10 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository orderings.add( switch (sort) { SortLocalAlbumsBy.id => OrderingTerm.asc(_db.localAlbumEntity.id), - SortLocalAlbumsBy.backupSelection => - OrderingTerm.asc(_db.localAlbumEntity.backupSelection), - SortLocalAlbumsBy.isIosSharedAlbum => - OrderingTerm.asc(_db.localAlbumEntity.isIosSharedAlbum), + SortLocalAlbumsBy.backupSelection => OrderingTerm.asc(_db.localAlbumEntity.backupSelection), + SortLocalAlbumsBy.isIosSharedAlbum => OrderingTerm.asc(_db.localAlbumEntity.isIosSharedAlbum), + SortLocalAlbumsBy.name => OrderingTerm.asc(_db.localAlbumEntity.name), + SortLocalAlbumsBy.assetCount => OrderingTerm.desc(assetCount), }, ); } @@ -49,30 +49,22 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository return query .map( - (row) => row - .readTable(_db.localAlbumEntity) - .toDto(assetCount: row.read(assetCount) ?? 0), + (row) => row.readTable(_db.localAlbumEntity).toDto(assetCount: row.read(assetCount) ?? 0), ) .get(); } - @override Future delete(String albumId) => transaction(() async { // Remove all assets that are only in this particular album // We cannot remove all assets in the album because they might be in other albums in iOS // That is not the case on Android since asset <-> album has one:one mapping - final assetsToDelete = _platform.isIOS - ? await _getUniqueAssetsInAlbum(albumId) - : await getAssetIds(albumId); + final assetsToDelete = _platform.isIOS ? await _getUniqueAssetsInAlbum(albumId) : await getAssetIds(albumId); await _deleteAssets(assetsToDelete); // All the other assets that are still associated will be unlinked automatically on-cascade - await _db.managers.localAlbumEntity - .filter((a) => a.id.equals(albumId)) - .delete(); + await _db.managers.localAlbumEntity.filter((a) => a.id.equals(albumId)).delete(); }); - @override Future syncDeletes( String albumId, Iterable assetIdsToKeep, @@ -88,20 +80,17 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository ..join([ innerJoin( _db.localAlbumEntity, - _db.localAlbumAssetEntity.albumId - .equalsExp(_db.localAlbumEntity.id), + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), ), ]); subQuery.where( - _db.localAlbumEntity.id.equals(albumId) & - _db.localAlbumAssetEntity.assetId.isNotIn(assetIdsToKeep), + _db.localAlbumEntity.id.equals(albumId) & _db.localAlbumAssetEntity.assetId.isNotIn(assetIdsToKeep), ); return localAsset.id.isInQuery(subQuery); }); await deleteSmt.go(); } - @override Future upsert( LocalAlbum localAlbum, { Iterable toUpsert = const [], @@ -116,8 +105,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository ); return _db.transaction(() async { - await _db.localAlbumEntity - .insertOne(companion, onConflict: DoUpdate((_) => companion)); + await _db.localAlbumEntity.insertOne(companion, onConflict: DoUpdate((_) => companion)); if (toUpsert.isNotEmpty) { await _upsertAssets(toUpsert); await _db.localAlbumAssetEntity.insertAll( @@ -134,12 +122,9 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository }); } - @override Future updateAll(Iterable albums) { return _db.transaction(() async { - await _db.localAlbumEntity - .update() - .write(const LocalAlbumEntityCompanion(marker_: Value(true))); + await _db.localAlbumEntity.update().write(const LocalAlbumEntityCompanion(marker_: Value(true))); await _db.batch((batch) { for (final album in albums) { @@ -155,7 +140,15 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository batch.insert( _db.localAlbumEntity, companion, - onConflict: DoUpdate((_) => companion), + onConflict: DoUpdate( + (old) => LocalAlbumEntityCompanion( + id: companion.id, + name: companion.name, + updatedAt: companion.updatedAt, + isIosSharedAlbum: companion.isIosSharedAlbum, + marker_: companion.marker_, + ), + ), ); } }); @@ -171,8 +164,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository ..join([ innerJoin( _db.localAlbumEntity, - _db.localAlbumAssetEntity.albumId - .equalsExp(_db.localAlbumEntity.id), + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), ), ]); subQuery.where(_db.localAlbumEntity.marker_.isNotNull()); @@ -185,7 +177,6 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository }); } - @override Future> getAssets(String albumId) { final query = _db.localAlbumAssetEntity.select().join( [ @@ -197,22 +188,16 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository ) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]); - return query - .map((row) => row.readTable(_db.localAssetEntity).toDto()) - .get(); + return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get(); } - @override Future> getAssetIds(String albumId) { final query = _db.localAlbumAssetEntity.selectOnly() ..addColumns([_db.localAlbumAssetEntity.assetId]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)); - return query - .map((row) => row.read(_db.localAlbumAssetEntity.assetId)!) - .get(); + return query.map((row) => row.read(_db.localAlbumAssetEntity.assetId)!).get(); } - @override Future processDelta({ required List updates, required List deletes, @@ -230,9 +215,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository assetAlbums.cast>().forEach((assetId, albumIds) { batch.deleteWhere( _db.localAlbumAssetEntity, - (f) => - f.albumId.isNotIn(albumIds.cast().nonNulls) & - f.assetId.equals(assetId), + (f) => f.albumId.isNotIn(albumIds.cast().nonNulls) & f.assetId.equals(assetId), ); }); }); @@ -253,7 +236,6 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository }); } - @override Future> getAssetsToHash(String albumId) { final query = _db.localAlbumAssetEntity.select().join( [ @@ -264,14 +246,11 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository ], ) ..where( - _db.localAlbumAssetEntity.albumId.equals(albumId) & - _db.localAssetEntity.checksum.isNull(), + _db.localAlbumAssetEntity.albumId.equals(albumId) & _db.localAssetEntity.checksum.isNull(), ) ..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]); - return query - .map((row) => row.readTable(_db.localAssetEntity).toDto()) - .get(); + return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get(); } Future _upsertAssets(Iterable localAssets) { @@ -290,6 +269,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository height: Value(asset.height), durationInSeconds: Value(asset.durationInSeconds), id: asset.id, + orientation: Value(asset.orientation), checksum: const Value(null), ); batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>( @@ -354,8 +334,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository ..addColumns([assetId]) ..groupBy( [assetId], - having: _db.localAlbumAssetEntity.albumId.count().equals(1) & - _db.localAlbumAssetEntity.albumId.equals(albumId), + having: _db.localAlbumAssetEntity.albumId.count().equals(1) & _db.localAlbumAssetEntity.albumId.equals(albumId), ); return query.map((row) => row.read(assetId)!).get(); @@ -370,31 +349,24 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository batch.deleteWhere(_db.localAssetEntity, (f) => f.id.isIn(ids)); }); } -} -extension on LocalAlbumEntityData { - LocalAlbum toDto({int assetCount = 0}) { - return LocalAlbum( - id: id, - name: name, - updatedAt: updatedAt, - assetCount: assetCount, - backupSelection: backupSelection, - ); - } -} - -extension on LocalAssetEntityData { - LocalAsset toDto() { - return LocalAsset( - id: id, - name: name, - checksum: checksum, - type: type, - createdAt: createdAt, - updatedAt: updatedAt, - durationInSeconds: durationInSeconds, - isFavorite: isFavorite, - ); + Future getThumbnail(String albumId) async { + final query = _db.localAlbumAssetEntity.select().join([ + innerJoin( + _db.localAssetEntity, + _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), + ), + ]) + ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) + ..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]) + ..limit(1); + + final results = await query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get(); + + return results.isNotEmpty ? results.first : null; + } + + Future getCount() { + return _db.managers.localAlbumEntity.count(); } } diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index 350a8dcd32..17521e1cba 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -1,15 +1,32 @@ +import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -class DriftLocalAssetRepository extends DriftDatabaseRepository - implements ILocalAssetRepository { +class DriftLocalAssetRepository extends DriftDatabaseRepository { final Drift _db; const DriftLocalAssetRepository(this._db) : super(_db); - @override + Stream watchAsset(String id) { + final query = _db.localAssetEntity.select().addColumns([_db.remoteAssetEntity.id]).join([ + leftOuterJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), + useColumns: false, + ), + ]) + ..where(_db.localAssetEntity.id.equals(id)); + + return query.map((row) { + final asset = row.readTable(_db.localAssetEntity).toDto(); + return asset.copyWith( + remoteId: row.read(_db.remoteAssetEntity.id), + ); + }).watchSingleOrNull(); + } + Future updateHashes(Iterable hashes) { if (hashes.isEmpty) { return Future.value(); @@ -25,4 +42,30 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository } }); } + + Future delete(List ids) { + if (ids.isEmpty) { + return Future.value(); + } + + return _db.batch((batch) { + for (final slice in ids.slices(32000)) { + batch.deleteWhere(_db.localAssetEntity, (e) => e.id.isIn(slice)); + } + }); + } + + Future getById(String id) { + final query = _db.localAssetEntity.select()..where((lae) => lae.id.equals(id)); + + return query.map((row) => row.toDto()).getSingleOrNull(); + } + + Future getCount() { + return _db.managers.localAssetEntity.count(); + } + + Future getHashedCount() { + return _db.managers.localAssetEntity.filter((e) => e.checksum.isNull().not()).count(); + } } diff --git a/mobile/lib/infrastructure/repositories/log.repository.dart b/mobile/lib/infrastructure/repositories/log.repository.dart index 6ff128f93b..7a909d90cb 100644 --- a/mobile/lib/infrastructure/repositories/log.repository.dart +++ b/mobile/lib/infrastructure/repositories/log.repository.dart @@ -1,28 +1,22 @@ -import 'package:immich_mobile/domain/interfaces/log.interface.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:isar/isar.dart'; -class IsarLogRepository extends IsarDatabaseRepository - implements ILogRepository { +class IsarLogRepository extends IsarDatabaseRepository { final Isar _db; const IsarLogRepository(super.db) : _db = db; - @override Future deleteAll() async { await transaction(() async => await _db.loggerMessages.clear()); return true; } - @override Future> getAll() async { - final logs = - await _db.loggerMessages.where().sortByCreatedAtDesc().findAll(); + final logs = await _db.loggerMessages.where().sortByCreatedAtDesc().findAll(); return logs.map((l) => l.toDto()).toList(); } - @override Future insert(LogMessage log) async { final logEntity = LoggerMessage.fromDto(log); await transaction(() async { @@ -31,17 +25,14 @@ class IsarLogRepository extends IsarDatabaseRepository return true; } - @override Future insertAll(Iterable logs) async { await transaction(() async { - final logEntities = - logs.map((log) => LoggerMessage.fromDto(log)).toList(); + final logEntities = logs.map((log) => LoggerMessage.fromDto(log)).toList(); await _db.loggerMessages.putAll(logEntities); }); return true; } - @override Future truncate({int limit = 250}) async { await transaction(() async { final count = await _db.loggerMessages.count(); diff --git a/mobile/lib/infrastructure/repositories/memory.repository.dart b/mobile/lib/infrastructure/repositories/memory.repository.dart new file mode 100644 index 0000000000..3663b75bf3 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/memory.repository.dart @@ -0,0 +1,121 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftMemoryRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftMemoryRepository(this._db) : super(_db); + + Future> getAll(String ownerId) async { + final now = DateTime.now(); + final localUtc = DateTime.utc(now.year, now.month, now.day, 0, 0, 0); + + final query = _db.select(_db.memoryEntity).join([ + leftOuterJoin( + _db.memoryAssetEntity, + _db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id), + ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.memoryAssetEntity.assetId) & + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline), + ), + ]) + ..where(_db.memoryEntity.ownerId.equals(ownerId)) + ..where(_db.memoryEntity.deletedAt.isNull()) + ..where( + _db.memoryEntity.showAt.isSmallerOrEqualValue(localUtc), + ) + ..where( + _db.memoryEntity.hideAt.isBiggerOrEqualValue(localUtc), + ) + ..orderBy([ + OrderingTerm.desc(_db.memoryEntity.memoryAt), + OrderingTerm.asc(_db.remoteAssetEntity.createdAt), + ]); + + final rows = await query.get(); + + final Map memoriesMap = {}; + + for (final row in rows) { + final memory = row.readTable(_db.memoryEntity); + final asset = row.readTable(_db.remoteAssetEntity); + + final existingMemory = memoriesMap[memory.id]; + if (existingMemory != null) { + existingMemory.assets.add(asset.toDto()); + } else { + final assets = [asset.toDto()]; + memoriesMap[memory.id] = memory.toDto().copyWith(assets: assets); + } + } + + return memoriesMap.values.toList(); + } + + Future get(String memoryId) async { + final query = _db.select(_db.memoryEntity).join([ + leftOuterJoin( + _db.memoryAssetEntity, + _db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id), + ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.memoryAssetEntity.assetId) & + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline), + ), + ]) + ..where(_db.memoryEntity.id.equals(memoryId)) + ..where(_db.memoryEntity.deletedAt.isNull()) + ..orderBy([ + OrderingTerm.desc(_db.memoryEntity.memoryAt), + OrderingTerm.asc(_db.remoteAssetEntity.createdAt), + ]); + + final rows = await query.get(); + + if (rows.isEmpty) { + return null; + } + + final memory = rows.first.readTable(_db.memoryEntity); + final assets = []; + + for (final row in rows) { + final asset = row.readTable(_db.remoteAssetEntity); + assets.add(asset.toDto()); + } + + return memory.toDto().copyWith(assets: assets); + } + + Future getCount() { + return _db.managers.memoryEntity.count(); + } +} + +extension on MemoryEntityData { + DriftMemory toDto() { + return DriftMemory( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + ownerId: ownerId, + type: type, + data: MemoryData.fromJson(data), + isSaved: isSaved, + memoryAt: memoryAt, + seenAt: seenAt, + showAt: showAt, + hideAt: hideAt, + assets: [], + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/partner.repository.dart b/mobile/lib/infrastructure/repositories/partner.repository.dart new file mode 100644 index 0000000000..9e78b1b65a --- /dev/null +++ b/mobile/lib/infrastructure/repositories/partner.repository.dart @@ -0,0 +1,157 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftPartnerRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftPartnerRepository(this._db) : super(_db); + + Future> getPartners(String userId) { + final query = _db.select(_db.partnerEntity).join([ + innerJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById), + ), + ]) + ..where( + _db.partnerEntity.sharedWithId.equals(userId), + ); + + return query.map((row) { + final user = row.readTable(_db.userEntity); + final partner = row.readTable(_db.partnerEntity); + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: partner.inTimeline, + ); + }).get(); + } + + // Get users who we can share our library with + Future> getAvailablePartners(String currentUserId) { + final query = _db.select(_db.userEntity)..where((row) => row.id.equals(currentUserId).not()); + + return query.map((user) { + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: false, + ); + }).get(); + } + + // Get users who are sharing their photos WITH the current user + Future> getSharedWith(String partnerId) { + final query = _db.select(_db.partnerEntity).join([ + innerJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById), + ), + ]) + ..where( + _db.partnerEntity.sharedWithId.equals(partnerId), + ); + + return query.map((row) { + final user = row.readTable(_db.userEntity); + final partner = row.readTable(_db.partnerEntity); + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: partner.inTimeline, + ); + }).get(); + } + + // Get users who the current user is sharing their photos TO + Future> getSharedBy(String userId) { + final query = _db.select(_db.partnerEntity).join([ + innerJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.partnerEntity.sharedWithId), + ), + ]) + ..where( + _db.partnerEntity.sharedById.equals(userId), + ); + + return query.map((row) { + final user = row.readTable(_db.userEntity); + final partner = row.readTable(_db.partnerEntity); + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: partner.inTimeline, + ); + }).get(); + } + + Future> getAllPartnerIds(String userId) async { + // Get users who are sharing with me (sharedWithId = userId) + final sharingWithMeQuery = _db.select(_db.partnerEntity)..where((tbl) => tbl.sharedWithId.equals(userId)); + final sharingWithMe = await sharingWithMeQuery.map((row) => row.sharedById).get(); + + // Get users who I am sharing with (sharedById = userId) + final sharingWithThemQuery = _db.select(_db.partnerEntity)..where((tbl) => tbl.sharedById.equals(userId)); + final sharingWithThem = await sharingWithThemQuery.map((row) => row.sharedWithId).get(); + + // Combine both lists and remove duplicates + final allPartnerIds = {...sharingWithMe, ...sharingWithThem}.toList(); + return allPartnerIds; + } + + Future getPartner(String partnerId, String userId) { + final query = _db.select(_db.partnerEntity).join([ + innerJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById), + ), + ]) + ..where( + _db.partnerEntity.sharedById.equals(partnerId) & _db.partnerEntity.sharedWithId.equals(userId), + ); + + return query.map((row) { + final user = row.readTable(_db.userEntity); + final partner = row.readTable(_db.partnerEntity); + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: partner.inTimeline, + ); + }).getSingleOrNull(); + } + + Future toggleShowInTimeline(PartnerUserDto partner, String userId) { + return _db.partnerEntity.update().replace( + PartnerEntityCompanion( + sharedById: Value(partner.id), + sharedWithId: Value(userId), + inTimeline: Value(!partner.inTimeline), + ), + ); + } + + Future create(String partnerId, String userId) { + final entity = PartnerEntityCompanion( + sharedById: Value(userId), + sharedWithId: Value(partnerId), + inTimeline: const Value(false), + ); + + return _db.partnerEntity.insertOne(entity); + } + + Future delete(String partnerId, String userId) { + return _db.partnerEntity.deleteWhere( + (t) => t.sharedById.equals(userId) & t.sharedWithId.equals(partnerId), + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/person.repository.dart b/mobile/lib/infrastructure/repositories/person.repository.dart new file mode 100644 index 0000000000..045fab4942 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/person.repository.dart @@ -0,0 +1,34 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/person.model.dart'; +import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftPersonRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftPersonRepository(this._db) : super(_db); + + Future> getAll(String userId) { + final query = _db.personEntity.select()..where((e) => e.ownerId.equals(userId)); + + return query.map((person) { + return person.toDto(); + }).get(); + } +} + +extension on PersonEntityData { + Person toDto() { + return Person( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + name: name, + faceAssetId: faceAssetId, + isFavorite: isFavorite, + isHidden: isHidden, + color: color, + birthDate: birthDate, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart new file mode 100644 index 0000000000..f60caceb47 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -0,0 +1,321 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +enum SortRemoteAlbumsBy { id, updatedAt } + +class DriftRemoteAlbumRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftRemoteAlbumRepository(this._db) : super(_db); + + Future> getAll({ + Set sortBy = const {SortRemoteAlbumsBy.updatedAt}, + }) { + final assetCount = _db.remoteAlbumAssetEntity.assetId.count(); + + final query = _db.remoteAlbumEntity.select().join([ + leftOuterJoin( + _db.remoteAlbumAssetEntity, + _db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id), + useColumns: false, + ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId), + useColumns: false, + ), + leftOuterJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), + useColumns: false, + ), + ]); + query + ..where(_db.remoteAssetEntity.deletedAt.isNull()) + ..addColumns([assetCount]) + ..addColumns([_db.userEntity.name]) + ..groupBy([_db.remoteAlbumEntity.id]); + + if (sortBy.isNotEmpty) { + final orderings = []; + for (final sort in sortBy) { + orderings.add( + switch (sort) { + SortRemoteAlbumsBy.id => OrderingTerm.asc(_db.remoteAlbumEntity.id), + SortRemoteAlbumsBy.updatedAt => OrderingTerm.desc(_db.remoteAlbumEntity.updatedAt), + }, + ); + } + query.orderBy(orderings); + } + + return query + .map( + (row) => row.readTable(_db.remoteAlbumEntity).toDto( + assetCount: row.read(assetCount) ?? 0, + ownerName: row.read(_db.userEntity.name)!, + ), + ) + .get(); + } + + Future get(String albumId) { + final assetCount = _db.remoteAlbumAssetEntity.assetId.count(); + + final query = _db.remoteAlbumEntity.select().join([ + leftOuterJoin( + _db.remoteAlbumAssetEntity, + _db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id), + useColumns: false, + ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId), + useColumns: false, + ), + leftOuterJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), + useColumns: false, + ), + ]) + ..where(_db.remoteAlbumEntity.id.equals(albumId) & _db.remoteAssetEntity.deletedAt.isNull()) + ..addColumns([assetCount]) + ..addColumns([_db.userEntity.name]) + ..groupBy([_db.remoteAlbumEntity.id]); + + return query + .map( + (row) => row.readTable(_db.remoteAlbumEntity).toDto( + assetCount: row.read(assetCount) ?? 0, + ownerName: row.read(_db.userEntity.name)!, + ), + ) + .getSingleOrNull(); + } + + Future create( + RemoteAlbum album, + List assetIds, + ) async { + await _db.transaction(() async { + final entity = RemoteAlbumEntityCompanion( + id: Value(album.id), + name: Value(album.name), + ownerId: Value(album.ownerId), + createdAt: Value(album.createdAt), + updatedAt: Value(album.updatedAt), + description: Value(album.description), + thumbnailAssetId: Value(album.thumbnailAssetId), + isActivityEnabled: Value(album.isActivityEnabled), + order: Value(album.order), + ); + + await _db.remoteAlbumEntity.insertOne(entity); + + if (assetIds.isNotEmpty) { + final albumAssets = assetIds.map( + (assetId) => RemoteAlbumAssetEntityCompanion( + albumId: Value(album.id), + assetId: Value(assetId), + ), + ); + + await _db.batch((batch) { + batch.insertAll( + _db.remoteAlbumAssetEntity, + albumAssets, + ); + }); + } + }); + } + + Future update(RemoteAlbum album) async { + await _db.remoteAlbumEntity.update().replace( + RemoteAlbumEntityCompanion( + id: Value(album.id), + name: Value(album.name), + ownerId: Value(album.ownerId), + createdAt: Value(album.createdAt), + updatedAt: Value(album.updatedAt), + description: Value(album.description), + thumbnailAssetId: Value(album.thumbnailAssetId), + isActivityEnabled: Value(album.isActivityEnabled), + order: Value(album.order), + ), + ); + } + + Future removeAssets(String albumId, List assetIds) { + return _db.remoteAlbumAssetEntity.deleteWhere( + (tbl) => tbl.albumId.equals(albumId) & tbl.assetId.isIn(assetIds), + ); + } + + FutureOr<(DateTime, DateTime)> getDateRange(String albumId) { + final query = _db.remoteAlbumAssetEntity.selectOnly() + ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)) + ..addColumns([ + _db.remoteAssetEntity.createdAt.min(), + _db.remoteAssetEntity.createdAt.max(), + ]) + ..join([ + innerJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId), + ), + ]); + + return query.map((row) { + final minDate = row.read(_db.remoteAssetEntity.createdAt.min()); + final maxDate = row.read(_db.remoteAssetEntity.createdAt.max()); + return (minDate ?? DateTime.now(), maxDate ?? DateTime.now()); + }).getSingle(); + } + + Future> getSharedUsers(String albumId) async { + final albumUserRows = + await (_db.select(_db.remoteAlbumUserEntity)..where((row) => row.albumId.equals(albumId))).get(); + + if (albumUserRows.isEmpty) { + return []; + } + + final userIds = albumUserRows.map((row) => row.userId); + + return (_db.select(_db.userEntity)..where((row) => row.id.isIn(userIds))) + .map( + (user) => UserDto( + id: user.id, + email: user.email, + name: user.name, + profileImagePath: user.profileImagePath?.isEmpty == true ? null : user.profileImagePath, + isAdmin: user.isAdmin, + updatedAt: user.updatedAt, + quotaSizeInBytes: user.quotaSizeInBytes ?? 0, + quotaUsageInBytes: user.quotaUsageInBytes, + memoryEnabled: true, + inTimeline: false, + isPartnerSharedBy: false, + isPartnerSharedWith: false, + ), + ) + .get(); + } + + Future> getAssets(String albumId) { + final query = _db.remoteAlbumAssetEntity.select().join([ + innerJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId), + ), + ]) + ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)); + + return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); + } + + Future addAssets(String albumId, List assetIds) async { + final albumAssets = assetIds.map( + (assetId) => RemoteAlbumAssetEntityCompanion( + albumId: Value(albumId), + assetId: Value(assetId), + ), + ); + + await _db.batch((batch) { + batch.insertAll( + _db.remoteAlbumAssetEntity, + albumAssets, + ); + }); + + return assetIds.length; + } + + Future addUsers(String albumId, List userIds) { + final albumUsers = userIds.map( + (assetId) => RemoteAlbumUserEntityCompanion( + albumId: Value(albumId), + userId: Value(assetId), + role: const Value(AlbumUserRole.editor), + ), + ); + + return _db.batch((batch) { + batch.insertAll( + _db.remoteAlbumUserEntity, + albumUsers, + ); + }); + } + + Future deleteAlbum(String albumId) async { + return _db.transaction(() async { + await _db.remoteAlbumEntity.deleteWhere( + (table) => table.id.equals(albumId), + ); + }); + } + + Stream watchAlbum(String albumId) { + final query = _db.remoteAlbumEntity.select().join([ + leftOuterJoin( + _db.remoteAlbumAssetEntity, + _db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id), + useColumns: false, + ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId), + useColumns: false, + ), + leftOuterJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), + useColumns: false, + ), + ]) + ..where(_db.remoteAlbumEntity.id.equals(albumId)) + ..addColumns([_db.userEntity.name]) + ..groupBy([_db.remoteAlbumEntity.id]); + + return query.map((row) { + final album = row.readTable(_db.remoteAlbumEntity).toDto( + ownerName: row.read(_db.userEntity.name)!, + ); + return album; + }).watchSingleOrNull(); + } + + Future getCount() { + return _db.managers.remoteAlbumEntity.count(); + } +} + +extension on RemoteAlbumEntityData { + RemoteAlbum toDto({int assetCount = 0, required String ownerName}) { + return RemoteAlbum( + id: id, + name: name, + ownerId: ownerId, + createdAt: createdAt, + updatedAt: updatedAt, + description: description, + thumbnailAssetId: thumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: order, + assetCount: assetCount, + ownerName: ownerName, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart new file mode 100644 index 0000000000..212b9b7f34 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -0,0 +1,250 @@ +import 'package:drift/drift.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/stack.model.dart'; +import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' hide ExifInfo; +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'; +import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; + +class RemoteAssetRepository extends DriftDatabaseRepository { + final Drift _db; + const RemoteAssetRepository(this._db) : super(_db); + + /// For testing purposes + Future> getSome(String userId) { + final query = _db.remoteAssetEntity.select() + ..where( + (row) => + _db.remoteAssetEntity.ownerId.equals(userId) & + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline), + ) + ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) + ..limit(10); + + return query.map((row) => row.toDto()).get(); + } + + SingleOrNullSelectable _assetSelectable(String id) { + final query = _db.remoteAssetEntity.select().addColumns([ + _db.localAssetEntity.id, + ]).join([ + leftOuterJoin( + _db.localAssetEntity, + _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), + useColumns: false, + ), + ]) + ..where(_db.remoteAssetEntity.id.equals(id)) + ..limit(1); + + return query.map((row) { + final asset = row.readTable(_db.remoteAssetEntity).toDto(); + return asset.copyWith(localId: row.read(_db.localAssetEntity.id)); + }); + } + + Stream watch(String id) { + return _assetSelectable(id).watchSingleOrNull(); + } + + Future get(String id) { + return _assetSelectable(id).getSingleOrNull(); + } + + Stream watchAsset(String id) { + final query = _db.remoteAssetEntity.select().addColumns([ + _db.localAssetEntity.id, + ]).join([ + leftOuterJoin( + _db.localAssetEntity, + _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), + useColumns: false, + ), + ]) + ..where(_db.remoteAssetEntity.id.equals(id)) + ..limit(1); + + return query.map((row) { + final asset = row.readTable(_db.remoteAssetEntity).toDto(); + return asset.copyWith(localId: row.read(_db.localAssetEntity.id)); + }).watchSingleOrNull(); + } + + Future> getStackChildren(RemoteAsset asset) { + if (asset.stackId == null) { + return Future.value([]); + } + + final query = _db.remoteAssetEntity.select() + ..where( + (row) => row.stackId.equals(asset.stackId!) & row.id.equals(asset.id).not(), + ) + ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]); + + return query.map((row) => row.toDto()).get(); + } + + Future getExif(String id) { + return _db.managers.remoteExifEntity + .filter((row) => row.assetId.id.equals(id)) + .map((row) => row.toDto()) + .getSingleOrNull(); + } + + Future> getPlaces() { + final asset = Subquery( + _db.remoteAssetEntity.select()..orderBy([(row) => OrderingTerm.desc(row.createdAt)]), + "asset", + ); + + final query = asset.selectOnly().join([ + innerJoin( + _db.remoteExifEntity, + _db.remoteExifEntity.assetId.equalsExp(asset.ref(_db.remoteAssetEntity.id)), + useColumns: false, + ), + ]) + ..addColumns([ + _db.remoteExifEntity.city, + _db.remoteExifEntity.assetId, + ]) + ..where( + _db.remoteExifEntity.city.isNotNull() & + asset.ref(_db.remoteAssetEntity.deletedAt).isNull() & + asset.ref(_db.remoteAssetEntity.visibility).equals(AssetVisibility.timeline.index), + ) + ..groupBy([_db.remoteExifEntity.city]) + ..orderBy([OrderingTerm.asc(_db.remoteExifEntity.city)]); + + return query.map((row) { + final assetId = row.read(_db.remoteExifEntity.assetId); + final city = row.read(_db.remoteExifEntity.city); + return (city!, assetId!); + }).get(); + } + + Future updateFavorite(List ids, bool isFavorite) { + return _db.batch((batch) async { + for (final id in ids) { + batch.update( + _db.remoteAssetEntity, + RemoteAssetEntityCompanion(isFavorite: Value(isFavorite)), + where: (e) => e.id.equals(id), + ); + } + }); + } + + Future updateVisibility(List ids, AssetVisibility visibility) { + return _db.batch((batch) async { + for (final id in ids) { + batch.update( + _db.remoteAssetEntity, + RemoteAssetEntityCompanion(visibility: Value(visibility)), + where: (e) => e.id.equals(id), + ); + } + }); + } + + Future trash(List ids) { + return _db.batch((batch) async { + for (final id in ids) { + batch.update( + _db.remoteAssetEntity, + RemoteAssetEntityCompanion(deletedAt: Value(DateTime.now())), + where: (e) => e.id.equals(id), + ); + } + }); + } + + Future restoreTrash(List ids) { + return _db.batch((batch) async { + for (final id in ids) { + batch.update( + _db.remoteAssetEntity, + const RemoteAssetEntityCompanion(deletedAt: Value(null)), + where: (e) => e.id.equals(id), + ); + } + }); + } + + Future delete(List ids) { + return _db.remoteAssetEntity.deleteWhere((row) => row.id.isIn(ids)); + } + + Future updateLocation(List ids, LatLng location) { + return _db.batch((batch) async { + for (final id in ids) { + batch.update( + _db.remoteExifEntity, + RemoteExifEntityCompanion( + latitude: Value(location.latitude), + longitude: Value(location.longitude), + ), + where: (e) => e.assetId.equals(id), + ); + } + }); + } + + Future stack(String userId, StackResponse stack) { + return _db.transaction(() async { + final stackIds = await _db.managers.stackEntity + .filter((row) => row.primaryAssetId.isIn(stack.assetIds)) + .map((row) => row.id) + .get(); + + await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds)); + + await _db.batch((batch) { + final companion = StackEntityCompanion( + ownerId: Value(userId), + primaryAssetId: Value(stack.primaryAssetId), + ); + + batch.insert( + _db.stackEntity, + companion.copyWith(id: Value(stack.id)), + onConflict: DoUpdate((_) => companion), + ); + + for (final assetId in stack.assetIds) { + batch.update( + _db.remoteAssetEntity, + RemoteAssetEntityCompanion( + stackId: Value(stack.id), + ), + where: (e) => e.id.equals(assetId), + ); + } + }); + }); + } + + Future unStack(List stackIds) { + return _db.transaction(() async { + await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds)); + + // TODO: delete this after adding foreign key on stackId + await _db.batch((batch) { + batch.update( + _db.remoteAssetEntity, + const RemoteAssetEntityCompanion(stackId: Value(null)), + where: (e) => e.stackId.isIn(stackIds), + ); + }); + }); + } + + Future getCount() { + return _db.managers.remoteAssetEntity.count(); + } +} diff --git a/mobile/lib/infrastructure/repositories/search_api.repository.dart b/mobile/lib/infrastructure/repositories/search_api.repository.dart new file mode 100644 index 0000000000..441219a02f --- /dev/null +++ b/mobile/lib/infrastructure/repositories/search_api.repository.dart @@ -0,0 +1,77 @@ +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' hide AssetVisibility; +import 'package:immich_mobile/infrastructure/repositories/api.repository.dart'; +import 'package:immich_mobile/models/search/search_filter.model.dart'; +import 'package:openapi/api.dart'; + +class SearchApiRepository extends ApiRepository { + final SearchApi _api; + const SearchApiRepository(this._api); + + Future search(SearchFilter filter, int page) { + AssetTypeEnum? type; + if (filter.mediaType.index == AssetType.image.index) { + type = AssetTypeEnum.IMAGE; + } else if (filter.mediaType.index == AssetType.video.index) { + type = AssetTypeEnum.VIDEO; + } + + if (filter.context != null && filter.context!.isNotEmpty) { + return _api.searchSmart( + SmartSearchDto( + query: filter.context!, + language: filter.language, + country: filter.location.country, + state: filter.location.state, + city: filter.location.city, + make: filter.camera.make, + model: filter.camera.model, + takenAfter: filter.date.takenAfter, + takenBefore: filter.date.takenBefore, + visibility: filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline, + isFavorite: filter.display.isFavorite ? true : null, + isNotInAlbum: filter.display.isNotInAlbum ? true : null, + personIds: filter.people.map((e) => e.id).toList(), + type: type, + page: page, + size: 1000, + ), + ); + } + + return _api.searchAssets( + MetadataSearchDto( + originalFileName: filter.filename != null && filter.filename!.isNotEmpty ? filter.filename : null, + country: filter.location.country, + description: filter.description != null && filter.description!.isNotEmpty ? filter.description : null, + state: filter.location.state, + city: filter.location.city, + make: filter.camera.make, + model: filter.camera.model, + takenAfter: filter.date.takenAfter, + takenBefore: filter.date.takenBefore, + visibility: filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline, + isFavorite: filter.display.isFavorite ? true : null, + isNotInAlbum: filter.display.isNotInAlbum ? true : null, + personIds: filter.people.map((e) => e.id).toList(), + type: type, + page: page, + size: 1000, + ), + ); + } + + Future?> getSearchSuggestions( + SearchSuggestionType type, { + String? country, + String? state, + String? make, + String? model, + }) => + _api.getSearchSuggestions( + type, + country: country, + state: state, + make: make, + model: model, + ); +} diff --git a/mobile/lib/infrastructure/repositories/stack.repository.dart b/mobile/lib/infrastructure/repositories/stack.repository.dart new file mode 100644 index 0000000000..cdac5fe4ad --- /dev/null +++ b/mobile/lib/infrastructure/repositories/stack.repository.dart @@ -0,0 +1,29 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/stack.model.dart'; +import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftStackRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftStackRepository(this._db) : super(_db); + + Future> getAll(String userId) { + final query = _db.stackEntity.select()..where((e) => e.ownerId.equals(userId)); + + return query.map((stack) { + return stack.toDto(); + }).get(); + } +} + +extension on StackEntityData { + Stack toDto() { + return Stack( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + primaryAssetId: primaryAssetId, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/storage.repository.dart b/mobile/lib/infrastructure/repositories/storage.repository.dart index 57dfc42135..0cf4f20ba8 100644 --- a/mobile/lib/infrastructure/repositories/storage.repository.dart +++ b/mobile/lib/infrastructure/repositories/storage.repository.dart @@ -1,31 +1,69 @@ import 'dart:io'; -import 'package:immich_mobile/domain/interfaces/storage.interface.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; -class StorageRepository implements IStorageRepository { - final _log = Logger('StorageRepository'); +class StorageRepository { + const StorageRepository(); - @override - Future getFileForAsset(LocalAsset asset) async { + Future getFileForAsset(String assetId) async { File? file; + final log = Logger('StorageRepository'); + try { - final entity = await AssetEntity.fromId(asset.id); + final entity = await AssetEntity.fromId(assetId); file = await entity?.originFile; if (file == null) { - _log.warning( - "Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", + log.warning("Cannot get file for asset $assetId"); + } + } catch (error, stackTrace) { + log.warning("Error getting file for asset $assetId", error, stackTrace); + } + return file; + } + + Future getMotionFileForAsset(LocalAsset asset) async { + File? file; + final log = Logger('StorageRepository'); + + try { + final entity = await AssetEntity.fromId(asset.id); + file = await entity?.originFileWithSubtype; + if (file == null) { + log.warning( + "Cannot get motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", ); } } catch (error, stackTrace) { - _log.warning( - "Error getting file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", + log.warning( + "Error getting motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", error, stackTrace, ); } return file; } + + Future getAssetEntityForAsset(LocalAsset asset) async { + final log = Logger('StorageRepository'); + + AssetEntity? entity; + + try { + entity = await AssetEntity.fromId(asset.id); + if (entity == null) { + log.warning( + "Cannot get AssetEntity for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", + ); + } + } catch (error, stackTrace) { + log.warning( + "Error getting AssetEntity for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", + error, + stackTrace, + ); + } + return entity; + } } diff --git a/mobile/lib/infrastructure/repositories/store.repository.dart b/mobile/lib/infrastructure/repositories/store.repository.dart index fec36193bc..990c2ad65d 100644 --- a/mobile/lib/infrastructure/repositories/store.repository.dart +++ b/mobile/lib/infrastructure/repositories/store.repository.dart @@ -1,4 +1,3 @@ -import 'package:immich_mobile/domain/interfaces/store.interface.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'; @@ -6,14 +5,12 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:isar/isar.dart'; -class IsarStoreRepository extends IsarDatabaseRepository - implements IStoreRepository { +class IsarStoreRepository extends IsarDatabaseRepository { 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(); @@ -21,7 +18,6 @@ class IsarStoreRepository extends IsarDatabaseRepository }); } - @override Stream> watchAll() { return _db.storeValues .filter() @@ -34,12 +30,10 @@ class IsarStoreRepository extends IsarDatabaseRepository ); } - @override Future delete(StoreKey key) async { return await transaction(() async => await _db.storeValues.delete(key.id)); } - @override Future insert(StoreKey key, T value) async { return await transaction(() async { await _db.storeValues.put(await _fromValue(key, value)); @@ -47,7 +41,6 @@ class IsarStoreRepository extends IsarDatabaseRepository }); } - @override Future tryGet(StoreKey key) async { final entity = (await _db.storeValues.get(key.id)); if (entity == null) { @@ -56,7 +49,6 @@ class IsarStoreRepository extends IsarDatabaseRepository return await _toValue(key, entity); } - @override Future update(StoreKey key, T value) async { return await transaction(() async { await _db.storeValues.put(await _fromValue(key, value)); @@ -64,7 +56,6 @@ class IsarStoreRepository extends IsarDatabaseRepository }); } - @override Stream watch(StoreKey key) async* { yield* _db.storeValues .watchObject(key.id, fireImmediately: true) @@ -72,23 +63,17 @@ class IsarStoreRepository extends IsarDatabaseRepository } Future> _toUpdateEvent(StoreValue entity) async { - final key = StoreKey.values.firstWhere((e) => e.id == entity.id) - as StoreKey; + 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) { + 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!), + 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?; @@ -109,12 +94,8 @@ class IsarStoreRepository extends IsarDatabaseRepository 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(); + final entities = await _db.storeValues.filter().anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)).findAll(); return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList()); } } diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index ca24eef60f..6727f19c64 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -3,24 +3,21 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; -class SyncApiRepository implements ISyncApiRepository { +class SyncApiRepository { final Logger _logger = Logger('SyncApiRepository'); final ApiService _api; SyncApiRepository(this._api); - @override Future ack(List data) { return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: data)); } - @override Future streamChanges( Function(List, Function() abort) onData, { int batchSize = kSyncEventBatchSize, @@ -45,11 +42,23 @@ class SyncApiRepository implements ISyncApiRepository { SyncStreamDto( types: [ SyncRequestType.usersV1, - SyncRequestType.partnersV1, SyncRequestType.assetsV1, - SyncRequestType.partnerAssetsV1, SyncRequestType.assetExifsV1, + SyncRequestType.partnersV1, + SyncRequestType.partnerAssetsV1, SyncRequestType.partnerAssetExifsV1, + SyncRequestType.albumsV1, + SyncRequestType.albumUsersV1, + SyncRequestType.albumAssetsV1, + SyncRequestType.albumAssetExifsV1, + SyncRequestType.albumToAssetsV1, + SyncRequestType.memoriesV1, + SyncRequestType.memoryToAssetsV1, + SyncRequestType.stacksV1, + SyncRequestType.partnerStacksV1, + SyncRequestType.userMetadataV1, + SyncRequestType.peopleV1, + SyncRequestType.assetFacesV1, ], ).toJson(), ); @@ -103,8 +112,7 @@ class SyncApiRepository implements ISyncApiRepository { client.close(); } stopwatch.stop(); - _logger - .info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); + _logger.info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); DLog.log("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); } @@ -138,6 +146,40 @@ const _kResponseMap = { SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson, SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson, SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson, + SyncEntityType.partnerAssetBackfillV1: SyncAssetV1.fromJson, SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson, SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson, + SyncEntityType.partnerAssetExifBackfillV1: SyncAssetExifV1.fromJson, + SyncEntityType.albumV1: SyncAlbumV1.fromJson, + SyncEntityType.albumDeleteV1: SyncAlbumDeleteV1.fromJson, + SyncEntityType.albumUserV1: SyncAlbumUserV1.fromJson, + SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson, + SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson, + SyncEntityType.albumAssetV1: SyncAssetV1.fromJson, + SyncEntityType.albumAssetBackfillV1: SyncAssetV1.fromJson, + SyncEntityType.albumAssetExifV1: SyncAssetExifV1.fromJson, + SyncEntityType.albumAssetExifBackfillV1: SyncAssetExifV1.fromJson, + SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson, + SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson, + SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson, + SyncEntityType.syncAckV1: _SyncAckV1.fromJson, + SyncEntityType.memoryV1: SyncMemoryV1.fromJson, + SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson, + SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson, + SyncEntityType.memoryToAssetDeleteV1: SyncMemoryAssetDeleteV1.fromJson, + SyncEntityType.stackV1: SyncStackV1.fromJson, + SyncEntityType.stackDeleteV1: SyncStackDeleteV1.fromJson, + SyncEntityType.partnerStackV1: SyncStackV1.fromJson, + SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson, + SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson, + SyncEntityType.userMetadataV1: SyncUserMetadataV1.fromJson, + SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson, + SyncEntityType.personV1: SyncPersonV1.fromJson, + SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson, + SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson, + SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson, }; + +class _SyncAckV1 { + static _SyncAckV1? fromJson(dynamic _) => _SyncAckV1(); +} diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 56f1631ee6..54bc01cfa2 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -1,13 +1,27 @@ +import 'dart:convert'; + import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/domain/models/user_metadata.model.dart'; +import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart' as api show AssetVisibility; -import 'package:openapi/api.dart' hide AssetVisibility; +import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey; +import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey; class SyncStreamRepository extends DriftDatabaseRepository { final Logger _logger = Logger('DriftSyncStreamRepository'); @@ -17,16 +31,9 @@ class SyncStreamRepository extends DriftDatabaseRepository { Future deleteUsersV1(Iterable data) async { try { - await _db.batch((batch) { - for (final user in data) { - batch.delete( - _db.userEntity, - UserEntityCompanion(id: Value(user.userId)), - ); - } - }); + await _db.userEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.userId))); } catch (error, stack) { - _logger.severe('Error while processing SyncUserDeleteV1', error, stack); + _logger.severe('Error: SyncUserDeleteV1', error, stack); rethrow; } } @@ -48,7 +55,7 @@ class SyncStreamRepository extends DriftDatabaseRepository { } }); } catch (error, stack) { - _logger.severe('Error while processing SyncUserV1', error, stack); + _logger.severe('Error: SyncUserV1', error, stack); rethrow; } } @@ -66,8 +73,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error while processing SyncPartnerDeleteV1', e, s); + } catch (error, stack) { + _logger.severe('Error: SyncPartnerDeleteV1', error, stack); rethrow; } } @@ -76,8 +83,7 @@ class SyncStreamRepository extends DriftDatabaseRepository { try { await _db.batch((batch) { for (final partner in data) { - final companion = - PartnerEntityCompanion(inTimeline: Value(partner.inTimeline)); + final companion = PartnerEntityCompanion(inTimeline: Value(partner.inTimeline)); batch.insert( _db.partnerEntity, @@ -89,76 +95,39 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error while processing SyncPartnerV1', e, s); + } catch (error, stack) { + _logger.severe('Error: SyncPartnerV1', error, stack); rethrow; } } - Future deleteAssetsV1(Iterable data) async { + Future deleteAssetsV1( + Iterable data, { + String debugLabel = 'user', + }) async { try { - await _deleteAssetsV1(data); - } catch (e, s) { - _logger.severe('Error while processing deleteAssetsV1', e, s); + await _db.remoteAssetEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.assetId)), + ); + } catch (error, stack) { + _logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack); rethrow; } } - Future updateAssetsV1(Iterable data) async { + Future updateAssetsV1( + Iterable data, { + String debugLabel = 'user', + }) async { try { - await _updateAssetsV1(data); - } catch (e, s) { - _logger.severe('Error while processing updateAssetsV1', e, s); - rethrow; - } - } - - Future deletePartnerAssetsV1(Iterable data) async { - try { - await _deleteAssetsV1(data); - } catch (e, s) { - _logger.severe('Error while processing deletePartnerAssetsV1', e, s); - rethrow; - } - } - - Future updatePartnerAssetsV1(Iterable data) async { - try { - await _updateAssetsV1(data); - } catch (e, s) { - _logger.severe('Error while processing updatePartnerAssetsV1', e, s); - rethrow; - } - } - - Future updateAssetsExifV1(Iterable data) async { - try { - await _updateAssetExifV1(data); - } catch (e, s) { - _logger.severe('Error while processing updateAssetsExifV1', e, s); - rethrow; - } - } - - Future updatePartnerAssetsExifV1(Iterable data) async { - try { - await _updateAssetExifV1(data); - } catch (e, s) { - _logger.severe('Error while processing updatePartnerAssetsExifV1', e, s); - rethrow; - } - } - - Future _updateAssetsV1(Iterable data) => - _db.batch((batch) { + await _db.batch((batch) { for (final asset in data) { final companion = RemoteAssetEntityCompanion( name: Value(asset.originalFileName), type: Value(asset.type.toAssetType()), createdAt: Value.absentIfNull(asset.fileCreatedAt), updatedAt: Value.absentIfNull(asset.fileModifiedAt), - durationInSeconds: - Value(asset.duration?.toDuration()?.inSeconds ?? 0), + durationInSeconds: Value(asset.duration?.toDuration()?.inSeconds ?? 0), checksum: Value(asset.checksum), isFavorite: Value(asset.isFavorite), ownerId: Value(asset.ownerId), @@ -166,6 +135,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { thumbHash: Value(asset.thumbhash), deletedAt: Value(asset.deletedAt), visibility: Value(asset.visibility.toAssetVisibility()), + livePhotoVideoId: Value(asset.livePhotoVideoId), + stackId: Value(asset.stackId), ); batch.insert( @@ -175,19 +146,18 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); + } catch (error, stack) { + _logger.severe('Error: updateAssetsV1 - $debugLabel', error, stack); + rethrow; + } + } - Future _deleteAssetsV1(Iterable assets) => - _db.batch((batch) { - for (final asset in assets) { - batch.delete( - _db.remoteAssetEntity, - RemoteAssetEntityCompanion(id: Value(asset.assetId)), - ); - } - }); - - Future _updateAssetExifV1(Iterable data) => - _db.batch((batch) { + Future updateAssetsExifV1( + Iterable data, { + String debugLabel = 'user', + }) async { + try { + await _db.batch((batch) { for (final exif in data) { final companion = RemoteExifEntityCompanion( city: Value(exif.city), @@ -201,8 +171,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { fNumber: Value(exif.fNumber), fileSize: Value(exif.fileSizeInByte), focalLength: Value(exif.focalLength), - latitude: Value(exif.latitude), - longitude: Value(exif.longitude), + latitude: Value(exif.latitude?.toDouble()), + longitude: Value(exif.longitude?.toDouble()), iso: Value(exif.iso), make: Value(exif.make), model: Value(exif.model), @@ -210,6 +180,7 @@ class SyncStreamRepository extends DriftDatabaseRepository { timeZone: Value(exif.timeZone), rating: Value(exif.rating), projectionType: Value(exif.projectionType), + lens: Value(exif.lensModel), ); batch.insert( @@ -219,6 +190,418 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); + } catch (error, stack) { + _logger.severe( + 'Error: updateAssetsExifV1 - $debugLabel', + error, + stack, + ); + rethrow; + } + } + + Future deleteAlbumsV1(Iterable data) async { + try { + await _db.remoteAlbumEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.albumId)), + ); + } catch (error, stack) { + _logger.severe('Error: deleteAlbumsV1', error, stack); + rethrow; + } + } + + Future updateAlbumsV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final album in data) { + final companion = RemoteAlbumEntityCompanion( + name: Value(album.name), + description: Value(album.description), + isActivityEnabled: Value(album.isActivityEnabled), + order: Value(album.order.toAlbumAssetOrder()), + thumbnailAssetId: Value(album.thumbnailAssetId), + ownerId: Value(album.ownerId), + createdAt: Value(album.createdAt), + updatedAt: Value(album.updatedAt), + ); + + batch.insert( + _db.remoteAlbumEntity, + companion.copyWith(id: Value(album.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateAlbumsV1', error, stack); + rethrow; + } + } + + Future deleteAlbumUsersV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final album in data) { + batch.delete( + _db.remoteAlbumUserEntity, + RemoteAlbumUserEntityCompanion( + albumId: Value(album.albumId), + userId: Value(album.userId), + ), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: deleteAlbumUsersV1', error, stack); + rethrow; + } + } + + Future updateAlbumUsersV1( + Iterable data, { + String debugLabel = 'user', + }) async { + try { + await _db.batch((batch) { + for (final album in data) { + final companion = RemoteAlbumUserEntityCompanion( + role: Value(album.role.toAlbumUserRole()), + ); + + batch.insert( + _db.remoteAlbumUserEntity, + companion.copyWith( + albumId: Value(album.albumId), + userId: Value(album.userId), + ), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe( + 'Error: updateAlbumUsersV1 - $debugLabel', + error, + stack, + ); + rethrow; + } + } + + Future deleteAlbumToAssetsV1( + Iterable data, + ) async { + try { + await _db.batch((batch) { + for (final album in data) { + batch.delete( + _db.remoteAlbumAssetEntity, + RemoteAlbumAssetEntityCompanion( + albumId: Value(album.albumId), + assetId: Value(album.assetId), + ), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: deleteAlbumToAssetsV1', error, stack); + rethrow; + } + } + + Future updateAlbumToAssetsV1( + Iterable data, { + String debugLabel = 'user', + }) async { + try { + await _db.batch((batch) { + for (final album in data) { + final companion = RemoteAlbumAssetEntityCompanion( + albumId: Value(album.albumId), + assetId: Value(album.assetId), + ); + + batch.insert( + _db.remoteAlbumAssetEntity, + companion, + onConflict: DoNothing(), + ); + } + }); + } catch (error, stack) { + _logger.severe( + 'Error: updateAlbumToAssetsV1 - $debugLabel', + error, + stack, + ); + rethrow; + } + } + + Future updateMemoriesV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final memory in data) { + final companion = MemoryEntityCompanion( + createdAt: Value(memory.createdAt), + deletedAt: Value(memory.deletedAt), + ownerId: Value(memory.ownerId), + type: Value(memory.type.toMemoryType()), + data: Value(jsonEncode(memory.data)), + isSaved: Value(memory.isSaved), + memoryAt: Value(memory.memoryAt), + seenAt: Value.absentIfNull(memory.seenAt), + showAt: Value.absentIfNull(memory.showAt), + hideAt: Value.absentIfNull(memory.hideAt), + ); + + batch.insert( + _db.memoryEntity, + companion.copyWith(id: Value(memory.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateMemoriesV1', error, stack); + rethrow; + } + } + + Future deleteMemoriesV1(Iterable data) async { + try { + await _db.memoryEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.memoryId)), + ); + } catch (error, stack) { + _logger.severe('Error: deleteMemoriesV1', error, stack); + rethrow; + } + } + + Future updateMemoryAssetsV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final asset in data) { + final companion = MemoryAssetEntityCompanion( + memoryId: Value(asset.memoryId), + assetId: Value(asset.assetId), + ); + + batch.insert( + _db.memoryAssetEntity, + companion, + onConflict: DoNothing(), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateMemoryAssetsV1', error, stack); + rethrow; + } + } + + Future deleteMemoryAssetsV1( + Iterable data, + ) async { + try { + await _db.batch((batch) { + for (final asset in data) { + batch.delete( + _db.memoryAssetEntity, + MemoryAssetEntityCompanion( + memoryId: Value(asset.memoryId), + assetId: Value(asset.assetId), + ), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: deleteMemoryAssetsV1', error, stack); + rethrow; + } + } + + Future updateStacksV1( + Iterable data, { + String debugLabel = 'user', + }) async { + try { + await _db.batch((batch) { + for (final stack in data) { + final companion = StackEntityCompanion( + createdAt: Value(stack.createdAt), + updatedAt: Value(stack.updatedAt), + ownerId: Value(stack.ownerId), + primaryAssetId: Value(stack.primaryAssetId), + ); + + batch.insert( + _db.stackEntity, + companion.copyWith(id: Value(stack.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateStacksV1 - $debugLabel', error, stack); + rethrow; + } + } + + Future deleteStacksV1( + Iterable data, { + String debugLabel = 'user', + }) async { + try { + await _db.stackEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.stackId)), + ); + } catch (error, stack) { + _logger.severe('Error: deleteStacksV1 - $debugLabel', error, stack); + rethrow; + } + } + + Future updateUserMetadatasV1( + Iterable data, + ) async { + try { + await _db.batch((batch) { + for (final userMetadata in data) { + final companion = UserMetadataEntityCompanion( + value: Value(userMetadata.value as Map), + ); + + batch.insert( + _db.userMetadataEntity, + companion.copyWith( + userId: Value(userMetadata.userId), + key: Value(userMetadata.key.toUserMetadataKey()), + ), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: deleteUserMetadatasV1', error, stack); + rethrow; + } + } + + Future deleteUserMetadatasV1( + Iterable data, + ) async { + try { + await _db.batch((batch) { + for (final userMetadata in data) { + batch.delete( + _db.userMetadataEntity, + UserMetadataEntityCompanion( + userId: Value(userMetadata.userId), + key: Value(userMetadata.key.toUserMetadataKey()), + ), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: deleteUserMetadatasV1', error, stack); + rethrow; + } + } + + Future updatePeopleV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final person in data) { + final companion = PersonEntityCompanion( + createdAt: Value(person.createdAt), + updatedAt: Value(person.updatedAt), + ownerId: Value(person.ownerId), + name: Value(person.name), + faceAssetId: Value(person.faceAssetId), + isFavorite: Value(person.isFavorite), + isHidden: Value(person.isHidden), + color: Value(person.color), + birthDate: Value(person.birthDate), + ); + + batch.insert( + _db.personEntity, + companion.copyWith(id: Value(person.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updatePeopleV1', error, stack); + rethrow; + } + } + + Future deletePeopleV1( + Iterable data, + ) async { + try { + await _db.batch((batch) { + for (final person in data) { + batch.deleteWhere( + _db.personEntity, + (row) => row.id.equals(person.personId), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: deletePeopleV1', error, stack); + rethrow; + } + } + + Future updateAssetFacesV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final assetFace in data) { + final companion = AssetFaceEntityCompanion( + assetId: Value(assetFace.assetId), + personId: Value(assetFace.personId), + imageWidth: Value(assetFace.imageWidth), + imageHeight: Value(assetFace.imageHeight), + boundingBoxX1: Value(assetFace.boundingBoxX1), + boundingBoxY1: Value(assetFace.boundingBoxY1), + boundingBoxX2: Value(assetFace.boundingBoxX2), + boundingBoxY2: Value(assetFace.boundingBoxY2), + sourceType: Value(assetFace.sourceType), + ); + + batch.insert( + _db.assetFaceEntity, + companion.copyWith(id: Value(assetFace.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateAssetFacesV1', error, stack); + rethrow; + } + } + + Future deleteAssetFacesV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final assetFace in data) { + batch.deleteWhere( + _db.assetFaceEntity, + (row) => row.id.equals(assetFace.assetFaceId), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: deleteAssetFacesV1', error, stack); + rethrow; + } + } } extension on AssetTypeEnum { @@ -231,6 +614,29 @@ extension on AssetTypeEnum { }; } +extension on AssetOrder { + AlbumAssetOrder toAlbumAssetOrder() => switch (this) { + AssetOrder.asc => AlbumAssetOrder.asc, + AssetOrder.desc => AlbumAssetOrder.desc, + _ => throw Exception('Unknown AssetOrder value: $this'), + }; +} + +extension on MemoryType { + MemoryTypeEnum toMemoryType() => switch (this) { + MemoryType.onThisDay => MemoryTypeEnum.onThisDay, + _ => throw Exception('Unknown MemoryType value: $this'), + }; +} + +extension on api.AlbumUserRole { + AlbumUserRole toAlbumUserRole() => switch (this) { + api.AlbumUserRole.editor => AlbumUserRole.editor, + api.AlbumUserRole.viewer => AlbumUserRole.viewer, + _ => throw Exception('Unknown AlbumUserRole value: $this'), + }; +} + extension on api.AssetVisibility { AssetVisibility toAssetVisibility() => switch (this) { api.AssetVisibility.timeline => AssetVisibility.timeline, @@ -241,12 +647,19 @@ extension on api.AssetVisibility { }; } +extension on api.UserMetadataKey { + UserMetadataKey toUserMetadataKey() => switch (this) { + api.UserMetadataKey.onboarding => UserMetadataKey.onboarding, + api.UserMetadataKey.preferences => UserMetadataKey.preferences, + api.UserMetadataKey.license => UserMetadataKey.license, + _ => throw Exception('Unknown UserMetadataKey value: $this'), + }; +} + extension on String { Duration? toDuration() { try { - final parts = split(':') - .map((e) => double.parse(e).toInt()) - .toList(growable: false); + final parts = split(':').map((e) => double.parse(e).toInt()).toList(growable: false); return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]); } catch (_) { diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 909332ec6e..772fb74f84 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -3,33 +3,48 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/interfaces/timeline.interface.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:stream_transform/stream_transform.dart'; -class DriftTimelineRepository extends DriftDatabaseRepository - implements ITimelineRepository { +class DriftTimelineRepository extends DriftDatabaseRepository { final Drift _db; const DriftTimelineRepository(super._db) : _db = _db; - List _generateBuckets(int count) { - final numBuckets = (count / kTimelineNoneSegmentSize).floor(); - final buckets = List.generate( - numBuckets, - (_) => const Bucket(assetCount: kTimelineNoneSegmentSize), - ); - if (count % kTimelineNoneSegmentSize != 0) { - buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize)); - } - return buckets; + Stream> watchTimelineUserIds(String userId) { + final query = _db.partnerEntity.selectOnly() + ..addColumns([_db.partnerEntity.sharedById]) + ..where( + _db.partnerEntity.inTimeline.equals(true) & _db.partnerEntity.sharedWithId.equals(userId), + ); + + return query + .map((row) => row.read(_db.partnerEntity.sharedById)!) + .watch() + // Add current user ID to the list + .map((users) => users..add(userId)); } - @override - Stream> watchMainBucket( + TimelineQuery main(List userIds, GroupAssetsBy groupBy) => ( + bucketSource: () => _watchMainBucket( + userIds, + groupBy: groupBy, + ), + assetSource: (offset, count) => _getMainBucketAssets( + userIds, + offset: offset, + count: count, + ), + ); + + Stream> _watchMainBucket( List userIds, { GroupAssetsBy groupBy = GroupAssetsBy.day, }) { @@ -49,20 +64,20 @@ class DriftTimelineRepository extends DriftDatabaseRepository .throttle(const Duration(seconds: 3), trailing: true); } - @override - Future> getMainBucketAssets( + Future> _getMainBucketAssets( List userIds, { required int offset, required int count, }) { return _db.mergedAssetDrift - .mergedAsset(userIds, limit: Limit(count, offset)) + .mergedAsset(userIds, limit: (_) => Limit(count, offset)) .map( - (row) => row.remoteId != null - ? Asset( + (row) => row.remoteId != null && row.ownerId != null + ? RemoteAsset( id: row.remoteId!, localId: row.localId, name: row.name, + ownerId: row.ownerId!, checksum: row.checksum, type: row.type, createdAt: row.createdAt, @@ -72,6 +87,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository height: row.height, isFavorite: row.isFavorite, durationInSeconds: row.durationInSeconds, + livePhotoVideoId: row.livePhotoVideoId, + stackId: row.stackId, ) : LocalAsset( id: row.localId!, @@ -85,13 +102,25 @@ class DriftTimelineRepository extends DriftDatabaseRepository height: row.height, isFavorite: row.isFavorite, durationInSeconds: row.durationInSeconds, + orientation: row.orientation, ), ) .get(); } - @override - Stream> watchLocalBucket( + TimelineQuery localAlbum(String albumId, GroupAssetsBy groupBy) => ( + bucketSource: () => _watchLocalAlbumBucket( + albumId, + groupBy: groupBy, + ), + assetSource: (offset, count) => _getLocalAlbumBucketAssets( + albumId, + offset: offset, + count: count, + ), + ); + + Stream> _watchLocalAlbumBucket( String albumId, { GroupAssetsBy groupBy = GroupAssetsBy.day, }) { @@ -105,14 +134,19 @@ class DriftTimelineRepository extends DriftDatabaseRepository final assetCountExp = _db.localAssetEntity.id.count(); final dateExp = _db.localAssetEntity.createdAt.dateFmt(groupBy); - final query = _db.localAssetEntity.selectOnly() + final query = _db.localAssetEntity.selectOnly().join([ + innerJoin( + _db.localAlbumAssetEntity, + _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), + useColumns: false, + ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), + useColumns: false, + ), + ]) ..addColumns([assetCountExp, dateExp]) - ..join([ - innerJoin( - _db.localAlbumAssetEntity, - _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), - ), - ]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..groupBy([dateExp]) ..orderBy([OrderingTerm.desc(dateExp)]); @@ -124,8 +158,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository }).watch(); } - @override - Future> getLocalBucketAssets( + Future> _getLocalAlbumBucketAssets( String albumId, { required int offset, required int count, @@ -135,16 +168,335 @@ class DriftTimelineRepository extends DriftDatabaseRepository innerJoin( _db.localAlbumAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), + useColumns: false, + ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), + useColumns: false, ), ], ) + ..addColumns([_db.remoteAssetEntity.id]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]) ..limit(count, offset: offset); - return query - .map((row) => row.readTable(_db.localAssetEntity).toDto()) - .get(); + + return query.map((row) { + final asset = row.readTable(_db.localAssetEntity).toDto(); + return asset.copyWith( + remoteId: row.read(_db.remoteAssetEntity.id), + ); + }).get(); } + + TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => ( + bucketSource: () => _watchRemoteAlbumBucket( + albumId, + groupBy: groupBy, + ), + assetSource: (offset, count) => _getRemoteAlbumBucketAssets( + albumId, + offset: offset, + count: count, + ), + ); + + Stream> _watchRemoteAlbumBucket( + String albumId, { + GroupAssetsBy groupBy = GroupAssetsBy.day, + }) { + if (groupBy == GroupAssetsBy.none) { + return _db.remoteAlbumAssetEntity + .count(where: (row) => row.albumId.equals(albumId)) + .map(_generateBuckets) + .watch() + .map((results) => results.isNotEmpty ? results.first : []) + .handleError((error) { + return []; + }); + } + + return (_db.remoteAlbumEntity.select()..where((row) => row.id.equals(albumId))).watch().switchMap((albums) { + if (albums.isEmpty) { + return Stream.value([]); + } + + final album = albums.first; + final isAscending = album.order == AlbumAssetOrder.asc; + final assetCountExp = _db.remoteAssetEntity.id.count(); + final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + + final query = _db.remoteAssetEntity.selectOnly() + ..addColumns([assetCountExp, dateExp]) + ..join([ + innerJoin( + _db.remoteAlbumAssetEntity, + _db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id), + useColumns: false, + ), + ]) + ..where( + _db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAlbumAssetEntity.albumId.equals(albumId), + ) + ..groupBy([dateExp]); + + if (isAscending) { + query.orderBy([OrderingTerm.asc(dateExp)]); + } else { + query.orderBy([OrderingTerm.desc(dateExp)]); + } + + return query.map((row) { + final timeline = row.read(dateExp)!.dateFmt(groupBy); + final assetCount = row.read(assetCountExp)!; + return TimeBucket(date: timeline, assetCount: assetCount); + }).watch(); + }).handleError((error) { + // If there's an error (e.g., album was deleted), return empty buckets + return []; + }); + } + + Future> _getRemoteAlbumBucketAssets( + String albumId, { + required int offset, + required int count, + }) async { + final albumData = await (_db.remoteAlbumEntity.select()..where((row) => row.id.equals(albumId))).getSingleOrNull(); + + // If album doesn't exist (was deleted), return empty list + if (albumData == null) { + return []; + } + + final isAscending = albumData.order == AlbumAssetOrder.asc; + + final query = _db.remoteAssetEntity.select().join( + [ + innerJoin( + _db.remoteAlbumAssetEntity, + _db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id), + useColumns: false, + ), + ], + )..where( + _db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAlbumAssetEntity.albumId.equals(albumId), + ); + + if (isAscending) { + query.orderBy([OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]); + } else { + query.orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]); + } + + query.limit(count, offset: offset); + + return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); + } + + TimelineQuery fromAssets(List assets) => ( + bucketSource: () => Stream.value(_generateBuckets(assets.length)), + assetSource: (offset, count) => Future.value(assets.skip(offset).take(count).toList()), + ); + + TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) => _remoteQueryBuilder( + filter: (row) => + row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.timeline) & row.ownerId.equals(ownerId), + groupBy: groupBy, + ); + + TimelineQuery favorite(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder( + filter: (row) => row.deletedAt.isNull() & row.isFavorite.equals(true) & row.ownerId.equals(userId), + groupBy: groupBy, + ); + + TimelineQuery trash(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder( + filter: (row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId), + groupBy: groupBy, + joinLocal: true, + ); + + TimelineQuery archived(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder( + filter: (row) => + row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive), + groupBy: groupBy, + ); + + TimelineQuery locked(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder( + filter: (row) => + row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.locked) & row.ownerId.equals(userId), + groupBy: groupBy, + ); + + TimelineQuery video(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder( + filter: (row) => + row.deletedAt.isNull() & + row.type.equalsValue(AssetType.video) & + row.visibility.equalsValue(AssetVisibility.timeline) & + row.ownerId.equals(userId), + groupBy: groupBy, + ); + + TimelineQuery place(String place, GroupAssetsBy groupBy) => ( + bucketSource: () => _watchPlaceBucket( + place, + groupBy: groupBy, + ), + assetSource: (offset, count) => _getPlaceBucketAssets( + place, + offset: offset, + count: count, + ), + ); + + Stream> _watchPlaceBucket( + String place, { + GroupAssetsBy groupBy = GroupAssetsBy.day, + }) { + if (groupBy == GroupAssetsBy.none) { + // TODO: implement GroupAssetBy for place + throw UnsupportedError( + "GroupAssetsBy.none is not supported for watchPlaceBucket", + ); + } + + final assetCountExp = _db.remoteAssetEntity.id.count(); + final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + + final query = _db.remoteAssetEntity.selectOnly() + ..addColumns([assetCountExp, dateExp]) + ..join([ + innerJoin( + _db.remoteExifEntity, + _db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id), + useColumns: false, + ), + ]) + ..where( + _db.remoteExifEntity.city.equals(place) & + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline), + ) + ..groupBy([dateExp]) + ..orderBy([OrderingTerm.desc(dateExp)]); + + return query.map((row) { + final timeline = row.read(dateExp)!.dateFmt(groupBy); + final assetCount = row.read(assetCountExp)!; + return TimeBucket(date: timeline, assetCount: assetCount); + }).watch(); + } + + Future> _getPlaceBucketAssets( + String place, { + required int offset, + required int count, + }) { + final query = _db.remoteAssetEntity.select().join( + [ + innerJoin( + _db.remoteExifEntity, + _db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id), + useColumns: false, + ), + ], + ) + ..where( + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & + _db.remoteExifEntity.city.equals(place), + ) + ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) + ..limit(count, offset: offset); + return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); + } + + TimelineQuery _remoteQueryBuilder({ + required Expression Function($RemoteAssetEntityTable row) filter, + GroupAssetsBy groupBy = GroupAssetsBy.day, + bool joinLocal = false, + }) { + return ( + bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy), + assetSource: (offset, count) => _getRemoteAssets( + filter: filter, + offset: offset, + count: count, + joinLocal: joinLocal, + ), + ); + } + + Stream> _watchRemoteBucket({ + required Expression Function($RemoteAssetEntityTable row) filter, + GroupAssetsBy groupBy = GroupAssetsBy.day, + }) { + if (groupBy == GroupAssetsBy.none) { + final query = _db.remoteAssetEntity.count(where: filter); + return query.map(_generateBuckets).watchSingle(); + } + + final assetCountExp = _db.remoteAssetEntity.id.count(); + final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + + final query = _db.remoteAssetEntity.selectOnly() + ..addColumns([assetCountExp, dateExp]) + ..where(filter(_db.remoteAssetEntity)) + ..groupBy([dateExp]) + ..orderBy([OrderingTerm.desc(dateExp)]); + + return query.map((row) { + final timeline = row.read(dateExp)!.dateFmt(groupBy); + final assetCount = row.read(assetCountExp)!; + return TimeBucket(date: timeline, assetCount: assetCount); + }).watch(); + } + + Future> _getRemoteAssets({ + required Expression Function($RemoteAssetEntityTable row) filter, + required int offset, + required int count, + bool joinLocal = false, + }) { + if (joinLocal) { + final query = _db.remoteAssetEntity.select().join([ + leftOuterJoin( + _db.localAssetEntity, + _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), + useColumns: false, + ), + ]) + ..addColumns([_db.localAssetEntity.id]) + ..where(filter(_db.remoteAssetEntity)) + ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) + ..limit(count, offset: offset); + + return query.map((row) { + final asset = row.readTable(_db.remoteAssetEntity).toDto(); + final localId = row.read(_db.localAssetEntity.id); + return asset.copyWith(localId: localId); + }).get(); + } else { + final query = _db.remoteAssetEntity.select() + ..where(filter) + ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) + ..limit(count, offset: offset); + + return query.map((row) => row.toDto()).get(); + } + } +} + +List _generateBuckets(int count) { + final buckets = List.generate( + (count / kTimelineNoneSegmentSize).floor(), + (_) => const Bucket(assetCount: kTimelineNoneSegmentSize), + ); + if (count % kTimelineNoneSegmentSize != 0) { + buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize)); + } + return buckets; } extension on Expression { @@ -153,7 +505,7 @@ extension on Expression { // to create the correct time bucket final localTimeExp = modify(const DateTimeModifier.localTime()); return switch (groupBy) { - GroupAssetsBy.day => localTimeExp.date, + GroupAssetsBy.day || GroupAssetsBy.auto => localTimeExp.date, GroupAssetsBy.month => localTimeExp.strftime("%Y-%m"), GroupAssetsBy.none => throw ArgumentError( "GroupAssetsBy.none is not supported for date formatting", @@ -165,7 +517,7 @@ extension on Expression { extension on String { DateTime dateFmt(GroupAssetsBy groupBy) { final format = switch (groupBy) { - GroupAssetsBy.day => "y-M-d", + GroupAssetsBy.day || GroupAssetsBy.auto => "y-M-d", GroupAssetsBy.month => "y-M", GroupAssetsBy.none => throw ArgumentError( "GroupAssetsBy.none is not supported for date formatting", diff --git a/mobile/lib/infrastructure/repositories/user.repository.dart b/mobile/lib/infrastructure/repositories/user.repository.dart index 4ccfccbdcc..2c6d721396 100644 --- a/mobile/lib/infrastructure/repositories/user.repository.dart +++ b/mobile/lib/infrastructure/repositories/user.repository.dart @@ -1,7 +1,6 @@ import 'package:immich_mobile/constants/enums.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/infrastructure/entities/user.entity.dart' as entity; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:isar/isar.dart'; diff --git a/mobile/lib/infrastructure/repositories/user_api.repository.dart b/mobile/lib/infrastructure/repositories/user_api.repository.dart index e990b9d8d1..0ee3deb4b1 100644 --- a/mobile/lib/infrastructure/repositories/user_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/user_api.repository.dart @@ -11,8 +11,7 @@ class UserApiRepository extends ApiRepository { const UserApiRepository(this._api); Future getMyUser() async { - final (adminDto, preferenceDto) = - await (_api.getMyUser(), _api.getMyPreferences()).wait; + final (adminDto, preferenceDto) = await (_api.getMyUser(), _api.getMyPreferences()).wait; if (adminDto == null) return null; return UserConverter.fromAdminDto(adminDto, preferenceDto); diff --git a/mobile/lib/infrastructure/repositories/user_metadata.repository.dart b/mobile/lib/infrastructure/repositories/user_metadata.repository.dart new file mode 100644 index 0000000000..81a4cd7945 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/user_metadata.repository.dart @@ -0,0 +1,37 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/user_metadata.model.dart'; +import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftUserMetadataRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftUserMetadataRepository(this._db) : super(_db); + + Future> getUserMetadata(String userId) { + final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(userId)); + + return query.map((userMetadata) { + return userMetadata.toDto(); + }).get(); + } +} + +extension on UserMetadataEntityData { + UserMetadata toDto() => switch (key) { + UserMetadataKey.onboarding => UserMetadata( + userId: userId, + key: key, + onboarding: Onboarding.fromMap(value), + ), + UserMetadataKey.preferences => UserMetadata( + userId: userId, + key: key, + preferences: Preferences.fromMap(value), + ), + UserMetadataKey.license => UserMetadata( + userId: userId, + key: key, + license: License.fromMap(value), + ), + }; +} diff --git a/mobile/lib/interfaces/album.interface.dart b/mobile/lib/interfaces/album.interface.dart deleted file mode 100644 index c1696eda80..0000000000 --- a/mobile/lib/interfaces/album.interface.dart +++ /dev/null @@ -1,55 +0,0 @@ -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/interfaces/database.interface.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; - -abstract interface class IAlbumRepository implements IDatabaseRepository { - Future create(Album album); - - Future get(int id); - - Future getByName( - String name, { - bool? shared, - bool? remote, - bool? owner, - }); - - Future> getAll({ - bool? shared, - bool? remote, - int? ownerId, - AlbumSort? sortBy, - }); - - Future update(Album album); - - Future delete(int albumId); - - Future deleteAllLocal(); - - Future count({bool? local}); - - Future addUsers(Album album, List users); - - Future removeUsers(Album album, List users); - - Future addAssets(Album album, List assets); - - Future removeAssets(Album album, List assets); - - Future recalculateMetadata(Album album); - - Future> search(String searchTerm, QuickFilterMode filterMode); - - Stream> watchRemoteAlbums(); - - Stream> watchLocalAlbums(); - - Stream watchAlbum(int id); - - Future clearTable(); -} - -enum AlbumSort { remoteId, localId } diff --git a/mobile/lib/interfaces/asset.interface.dart b/mobile/lib/interfaces/asset.interface.dart deleted file mode 100644 index ca9e9d64fb..0000000000 --- a/mobile/lib/interfaces/asset.interface.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/interfaces/database.interface.dart'; - -abstract interface class IAssetRepository implements IDatabaseRepository { - Future getByRemoteId(String id); - - Future getByOwnerIdChecksum(int ownerId, String checksum); - - Future> getAllByRemoteId( - Iterable ids, { - AssetState? state, - }); - - Future> getAllByOwnerIdChecksum( - List ids, - List checksums, - ); - - Future> getAll({ - required String ownerId, - AssetState? state, - AssetSort? sortBy, - int? limit, - }); - - Future> getAllLocal(); - - Future> getByAlbum( - Album album, { - Iterable notOwnedBy = const [], - String? ownerId, - AssetState? state, - AssetSort? sortBy, - }); - - Future update(Asset asset); - - Future> updateAll(List assets); - - Future deleteAllByRemoteId(List ids, {AssetState? state}); - - Future deleteByIds(List ids); - - Future> getMatches({ - required List assets, - required String ownerId, - AssetState? state, - int limit = 100, - }); - - Future upsertDuplicatedAssets(Iterable duplicatedAssets); - - Future> getAllDuplicatedAssetIds(); - - Future> getStackAssets(String stackId); - - Future clearTable(); - - Stream watchAsset(int id, {bool fireImmediately = false}); - - Future> getTrashAssets(String userId); - - Future> getRecentlyTakenAssets(String userId); - Future> getMotionAssets(String userId); -} - -enum AssetSort { checksum, ownerIdChecksum } diff --git a/mobile/lib/interfaces/asset_api.interface.dart b/mobile/lib/interfaces/asset_api.interface.dart deleted file mode 100644 index 71ee993a6b..0000000000 --- a/mobile/lib/interfaces/asset_api.interface.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -abstract interface class IAssetApiRepository { - // Future get(String id); - - // Future> getAll(); - - // Future create(Asset asset); - - Future update( - String id, { - String? description, - }); - - // Future delete(String id); - - Future> search({List personIds = const []}); - - Future updateVisibility( - List list, - AssetVisibilityEnum visibility, - ); - - Future getAssetMIMEType(String id); -} diff --git a/mobile/lib/interfaces/asset_media.interface.dart b/mobile/lib/interfaces/asset_media.interface.dart deleted file mode 100644 index 2606d5c23c..0000000000 --- a/mobile/lib/interfaces/asset_media.interface.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -abstract interface class IAssetMediaRepository { - Future> deleteAll(List ids); - - Future get(String id); - - /// Obtaining the correct original filename of the asset - Future getOriginalFilename(String id); -} diff --git a/mobile/lib/interfaces/auth.interface.dart b/mobile/lib/interfaces/auth.interface.dart deleted file mode 100644 index 57088f4569..0000000000 --- a/mobile/lib/interfaces/auth.interface.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:immich_mobile/interfaces/database.interface.dart'; -import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; - -abstract interface class IAuthRepository implements IDatabaseRepository { - Future clearLocalData(); - String getAccessToken(); - bool getEndpointSwitchingFeature(); - String? getPreferredWifiName(); - String? getLocalEndpoint(); - List getExternalEndpointList(); -} diff --git a/mobile/lib/interfaces/auth_api.interface.dart b/mobile/lib/interfaces/auth_api.interface.dart deleted file mode 100644 index bb9a8b5a2c..0000000000 --- a/mobile/lib/interfaces/auth_api.interface.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/models/auth/login_response.model.dart'; - -abstract interface class IAuthApiRepository { - Future login(String email, String password); - - Future logout(); - - Future changePassword(String newPassword); - - Future unlockPinCode(String pinCode); - Future lockPinCode(); - - Future setupPinCode(String pinCode); -} diff --git a/mobile/lib/interfaces/backup_album.interface.dart b/mobile/lib/interfaces/backup_album.interface.dart deleted file mode 100644 index f98adb6821..0000000000 --- a/mobile/lib/interfaces/backup_album.interface.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/interfaces/database.interface.dart'; - -abstract interface class IBackupAlbumRepository implements IDatabaseRepository { - Future> getAll({BackupAlbumSort? sort}); - - Future> getIdsBySelection(BackupSelection backup); - - Future> getAllBySelection(BackupSelection backup); - - Future updateAll(List backupAlbums); - - Future deleteAll(List ids); -} - -enum BackupAlbumSort { id } diff --git a/mobile/lib/interfaces/cast_destination_service.interface.dart b/mobile/lib/interfaces/cast_destination_service.interface.dart deleted file mode 100644 index add8ad7c51..0000000000 --- a/mobile/lib/interfaces/cast_destination_service.interface.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/models/cast/cast_manager_state.dart'; - -abstract interface class ICastDestinationService { - Future initialize(); - CastDestinationType getType(); - - void Function(bool)? onConnectionState; - - void Function(Duration)? onCurrentTime; - void Function(Duration)? onDuration; - - void Function(String)? onReceiverName; - void Function(CastState)? onCastState; - - Future connect(dynamic device); - - void loadMedia(Asset asset, bool reload); - - void play(); - void pause(); - void seekTo(Duration position); - void stop(); - Future disconnect(); - - Future> getDevices(); -} diff --git a/mobile/lib/interfaces/etag.interface.dart b/mobile/lib/interfaces/etag.interface.dart deleted file mode 100644 index 8b4b5806c9..0000000000 --- a/mobile/lib/interfaces/etag.interface.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/interfaces/database.interface.dart'; - -abstract interface class IETagRepository implements IDatabaseRepository { - Future get(String id); - - Future getById(String id); - - Future> getAllIds(); - - Future upsertAll(List etags); - - Future deleteByIds(List ids); - - Future clearTable(); -} diff --git a/mobile/lib/interfaces/file_media.interface.dart b/mobile/lib/interfaces/file_media.interface.dart deleted file mode 100644 index ea01819dc3..0000000000 --- a/mobile/lib/interfaces/file_media.interface.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:immich_mobile/entities/asset.entity.dart'; - -abstract interface class IFileMediaRepository { - Future saveImage( - Uint8List data, { - required String title, - String? relativePath, - }); - - Future saveImageWithFile( - String filePath, { - String? title, - String? relativePath, - }); - - Future saveVideo( - File file, { - required String title, - String? relativePath, - }); - - Future saveLivePhoto({ - required File image, - required File video, - required String title, - }); - - Future clearFileCache(); - - Future enableBackgroundAccess(); - - Future requestExtendedPermissions(); -} diff --git a/mobile/lib/interfaces/folder_api.interface.dart b/mobile/lib/interfaces/folder_api.interface.dart deleted file mode 100644 index 68c1652e21..0000000000 --- a/mobile/lib/interfaces/folder_api.interface.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -abstract interface class IFolderApiRepository { - Future> getAllUniquePaths(); - Future> getAssetsForPath(String? path); -} diff --git a/mobile/lib/interfaces/local_files_manager.interface.dart b/mobile/lib/interfaces/local_files_manager.interface.dart deleted file mode 100644 index 07274b7e29..0000000000 --- a/mobile/lib/interfaces/local_files_manager.interface.dart +++ /dev/null @@ -1,5 +0,0 @@ -abstract interface class ILocalFilesManager { - Future moveToTrash(List mediaUrls); - Future restoreFromTrash(String fileName, int type); - Future requestManageMediaPermission(); -} diff --git a/mobile/lib/interfaces/upload.interface.dart b/mobile/lib/interfaces/upload.interface.dart deleted file mode 100644 index d4b2298a14..0000000000 --- a/mobile/lib/interfaces/upload.interface.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:background_downloader/background_downloader.dart'; - -abstract interface class IUploadRepository { - void Function(TaskStatusUpdate)? onUploadStatus; - void Function(TaskProgressUpdate)? onTaskProgress; - - Future upload(UploadTask task); - Future cancel(String id); - Future deleteAllTrackingRecords(); - Future deleteRecordsWithIds(List id); -} diff --git a/mobile/lib/interfaces/widget.interface.dart b/mobile/lib/interfaces/widget.interface.dart deleted file mode 100644 index f76fbef8de..0000000000 --- a/mobile/lib/interfaces/widget.interface.dart +++ /dev/null @@ -1,5 +0,0 @@ -abstract interface class IWidgetRepository { - Future saveData(String key, String value); - Future refresh(String name); - Future setAppGroupId(String appGroupId); -} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index bc9edcd46e..e52f298413 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:auto_route/auto_route.dart'; import 'package:background_downloader/background_downloader.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -9,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; @@ -17,17 +19,19 @@ import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provide import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/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'; import 'package:immich_mobile/utils/cache/widgets_binding.dart'; -import 'package:immich_mobile/utils/download.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; +import 'package:immich_mobile/utils/licenses.dart'; import 'package:immich_mobile/utils/migration.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:logging/logging.dart'; @@ -89,12 +93,30 @@ Future initApp() async { initializeTimeZones(); + // Initialize the file downloader + + await FileDownloader().configure( + // maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3 + globalConfig: (Config.holdingQueue, (1000, 1000, 1000)), + ); + await FileDownloader().trackTasksInGroup( - downloadGroupLivePhoto, + kDownloadGroupLivePhoto, markDownloadedComplete: false, ); await FileDownloader().trackTasks(); + + LicenseRegistry.addLicense( + () async* { + for (final license in nonPubLicenses.entries) { + yield LicenseEntryWithLineBreaks( + [license.key], + license.value, + ); + } + }, + ); } class ImmichApp extends ConsumerStatefulWidget { @@ -104,8 +126,7 @@ class ImmichApp extends ConsumerStatefulWidget { ImmichAppState createState() => ImmichAppState(); } -class ImmichAppState extends ConsumerState - with WidgetsBindingObserver { +class ImmichAppState extends ConsumerState with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { @@ -146,9 +167,7 @@ class ImmichAppState extends ConsumerState // Android 8 does not support transparent app bars final info = await DeviceInfoPlugin().androidInfo; if (info.version.sdkInt <= 26) { - overlayStyle = context.isDarkTheme - ? SystemUiOverlayStyle.dark - : SystemUiOverlayStyle.light; + overlayStyle = context.isDarkTheme ? SystemUiOverlayStyle.dark : SystemUiOverlayStyle.light; } } SystemChrome.setSystemUIOverlayStyle(overlayStyle); @@ -156,7 +175,8 @@ class ImmichAppState extends ConsumerState } void _configureFileDownloaderNotifications() { - FileDownloader().configureNotification( + FileDownloader().configureNotificationForGroup( + kDownloadGroupImage, running: TaskNotification( 'downloading_media'.tr(), '${'file_name'.tr()}: {filename}', @@ -167,6 +187,59 @@ class ImmichAppState extends ConsumerState ), progressBar: true, ); + + FileDownloader().configureNotificationForGroup( + kDownloadGroupVideo, + running: TaskNotification( + 'downloading_media'.tr(), + '${'file_name'.tr()}: {filename}', + ), + complete: TaskNotification( + 'download_finished'.tr(), + '${'file_name'.tr()}: {filename}', + ), + progressBar: true, + ); + + FileDownloader().configureNotificationForGroup( + kManualUploadGroup, + running: TaskNotification( + 'uploading_media'.tr(), + '${'file_name'.tr()}: {displayName}', + ), + complete: TaskNotification( + 'upload_finished'.tr(), + '${'file_name'.tr()}: {displayName}', + ), + progressBar: true, + ); + } + + Future _deepLinkBuilder(PlatformDeepLink deepLink) async { + final deepLinkHandler = ref.read(deepLinkServiceProvider); + final currentRouteName = ref.read(currentRouteNameProvider.notifier).state; + + final isColdStart = currentRouteName == null || currentRouteName == SplashScreenRoute.name; + + if (deepLink.uri.scheme == "immich") { + final proposedRoute = await deepLinkHandler.handleScheme( + deepLink, + isColdStart, + ); + + return proposedRoute; + } + + if (deepLink.uri.host == "my.immich.app") { + final proposedRoute = await deepLinkHandler.handleMyImmichApp( + deepLink, + isColdStart, + ); + + return proposedRoute; + } + + return DeepLink.path(deepLink.path); } @override @@ -220,9 +293,9 @@ class ImmichAppState extends ConsumerState colorScheme: immichTheme.light, locale: context.locale, ), - routeInformationParser: router.defaultRouteParser(), - routerDelegate: router.delegate( - navigatorObservers: () => [AppNavigationObserver(ref: ref)], + routerConfig: router.config( + deepLinkBuilder: _deepLinkBuilder, + navigatorObservers: () => [AppNavigationObserver(ref: ref), HeroController()], ), ), ); diff --git a/mobile/lib/models/activities/activity.model.dart b/mobile/lib/models/activities/activity.model.dart index 17f70d5d62..38c2bef77a 100644 --- a/mobile/lib/models/activities/activity.model.dart +++ b/mobile/lib/models/activities/activity.model.dart @@ -56,12 +56,7 @@ class Activity { @override int get hashCode { - return id.hashCode ^ - assetId.hashCode ^ - comment.hashCode ^ - createdAt.hashCode ^ - type.hashCode ^ - user.hashCode; + return id.hashCode ^ assetId.hashCode ^ comment.hashCode ^ createdAt.hashCode ^ type.hashCode ^ user.hashCode; } } diff --git a/mobile/lib/models/albums/album_add_asset_response.model.dart b/mobile/lib/models/albums/album_add_asset_response.model.dart index 26168c957c..fbc8a4d560 100644 --- a/mobile/lib/models/albums/album_add_asset_response.model.dart +++ b/mobile/lib/models/albums/album_add_asset_response.model.dart @@ -32,16 +32,14 @@ class AlbumAddAssetsResponse { String toJson() => json.encode(toMap()); @override - String toString() => - 'AddAssetsResponse(alreadyInAlbum: $alreadyInAlbum, successfullyAdded: $successfullyAdded)'; + 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; + return listEquals(other.alreadyInAlbum, alreadyInAlbum) && other.successfullyAdded == successfullyAdded; } @override diff --git a/mobile/lib/models/albums/album_viewer_page_state.model.dart b/mobile/lib/models/albums/album_viewer_page_state.model.dart index 10a8183ddc..823226b4a4 100644 --- a/mobile/lib/models/albums/album_viewer_page_state.model.dart +++ b/mobile/lib/models/albums/album_viewer_page_state.model.dart @@ -5,7 +5,7 @@ class AlbumViewerPageState { final String editTitleText; final String editDescriptionText; - AlbumViewerPageState({ + const AlbumViewerPageState({ required this.isEditAlbum, required this.editTitleText, required this.editDescriptionText, @@ -43,8 +43,7 @@ class AlbumViewerPageState { String toJson() => json.encode(toMap()); - factory AlbumViewerPageState.fromJson(String source) => - AlbumViewerPageState.fromMap(json.decode(source)); + factory AlbumViewerPageState.fromJson(String source) => AlbumViewerPageState.fromMap(json.decode(source)); @override String toString() => @@ -61,8 +60,5 @@ class AlbumViewerPageState { } @override - int get hashCode => - isEditAlbum.hashCode ^ - editTitleText.hashCode ^ - editDescriptionText.hashCode; + 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 index 04934f7a72..d921ac63ce 100644 --- a/mobile/lib/models/albums/asset_selection_page_result.model.dart +++ b/mobile/lib/models/albums/asset_selection_page_result.model.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/entities/asset.entity.dart'; class AssetSelectionPageResult { final Set selectedAssets; - AssetSelectionPageResult({ + const AssetSelectionPageResult({ required this.selectedAssets, }); @override @@ -12,8 +12,7 @@ class AssetSelectionPageResult { if (identical(this, other)) return true; final setEquals = const DeepCollectionEquality().equals; - return other is AssetSelectionPageResult && - setEquals(other.selectedAssets, selectedAssets); + return other is AssetSelectionPageResult && setEquals(other.selectedAssets, selectedAssets); } @override diff --git a/mobile/lib/models/asset_selection_state.dart b/mobile/lib/models/asset_selection_state.dart index b080dca003..c022ec3a3a 100644 --- a/mobile/lib/models/asset_selection_state.dart +++ b/mobile/lib/models/asset_selection_state.dart @@ -48,9 +48,5 @@ class AssetSelectionState { } @override - int get hashCode => - hasRemote.hashCode ^ - hasLocal.hashCode ^ - hasMerged.hashCode ^ - selectedCount.hashCode; + int get hashCode => hasRemote.hashCode ^ hasLocal.hashCode ^ hasMerged.hashCode ^ selectedCount.hashCode; } diff --git a/mobile/lib/models/auth/auth_state.model.dart b/mobile/lib/models/auth/auth_state.model.dart index fb65850f1d..0d8357d66d 100644 --- a/mobile/lib/models/auth/auth_state.model.dart +++ b/mobile/lib/models/auth/auth_state.model.dart @@ -7,7 +7,7 @@ class AuthState { final bool isAdmin; final String profileImagePath; - AuthState({ + const AuthState({ required this.deviceId, required this.userId, required this.userEmail, diff --git a/mobile/lib/models/auth/auxilary_endpoint.model.dart b/mobile/lib/models/auth/auxilary_endpoint.model.dart index 89aba60913..e876097d61 100644 --- a/mobile/lib/models/auth/auxilary_endpoint.model.dart +++ b/mobile/lib/models/auth/auxilary_endpoint.model.dart @@ -5,7 +5,7 @@ class AuxilaryEndpoint { final String url; final AuxCheckStatus status; - AuxilaryEndpoint({ + const AuxilaryEndpoint({ required this.url, required this.status, }); @@ -55,7 +55,7 @@ class AuxilaryEndpoint { class AuxCheckStatus { final String name; - AuxCheckStatus({ + const AuxCheckStatus({ required this.name, }); const AuxCheckStatus._(this.name); @@ -97,8 +97,7 @@ class AuxCheckStatus { String toJson() => json.encode(toMap()); - factory AuxCheckStatus.fromJson(String source) => - AuxCheckStatus.fromMap(json.decode(source) as Map); + factory AuxCheckStatus.fromJson(String source) => AuxCheckStatus.fromMap(json.decode(source) as Map); @override String toString() => 'AuxCheckStatus(name: $name)'; diff --git a/mobile/lib/models/auth/biometric_status.model.dart b/mobile/lib/models/auth/biometric_status.model.dart index 3057f06e9c..223b283279 100644 --- a/mobile/lib/models/auth/biometric_status.model.dart +++ b/mobile/lib/models/auth/biometric_status.model.dart @@ -11,8 +11,7 @@ class BiometricStatus { }); @override - String toString() => - 'BiometricStatus(availableBiometrics: $availableBiometrics, canAuthenticate: $canAuthenticate)'; + String toString() => 'BiometricStatus(availableBiometrics: $availableBiometrics, canAuthenticate: $canAuthenticate)'; BiometricStatus copyWith({ List? availableBiometrics, @@ -29,8 +28,7 @@ class BiometricStatus { if (identical(this, other)) return true; final listEquals = const DeepCollectionEquality().equals; - return listEquals(other.availableBiometrics, availableBiometrics) && - other.canAuthenticate == canAuthenticate; + return listEquals(other.availableBiometrics, availableBiometrics) && other.canAuthenticate == canAuthenticate; } @override diff --git a/mobile/lib/models/auth/login_response.model.dart b/mobile/lib/models/auth/login_response.model.dart index f1398418ca..0aa9c0b349 100644 --- a/mobile/lib/models/auth/login_response.model.dart +++ b/mobile/lib/models/auth/login_response.model.dart @@ -13,7 +13,7 @@ class LoginResponse { final String userId; - LoginResponse({ + const LoginResponse({ required this.accessToken, required this.isAdmin, required this.name, diff --git a/mobile/lib/models/backup/available_album.model.dart b/mobile/lib/models/backup/available_album.model.dart index c75d6446d8..96a19cf60b 100644 --- a/mobile/lib/models/backup/available_album.model.dart +++ b/mobile/lib/models/backup/available_album.model.dart @@ -4,7 +4,7 @@ class AvailableAlbum { final Album album; final int assetCount; final DateTime? lastBackup; - AvailableAlbum({ + const AvailableAlbum({ required this.album, required this.assetCount, this.lastBackup, @@ -29,8 +29,7 @@ class AvailableAlbum { bool get isAll => album.isAll; @override - String toString() => - 'AvailableAlbum(albumEntity: $album, lastBackup: $lastBackup)'; + String toString() => 'AvailableAlbum(albumEntity: $album, lastBackup: $lastBackup)'; @override bool operator ==(Object other) { diff --git a/mobile/lib/models/backup/backup_state.model.dart b/mobile/lib/models/backup/backup_state.model.dart index d829f411fc..39554736dd 100644 --- a/mobile/lib/models/backup/backup_state.model.dart +++ b/mobile/lib/models/backup/backup_state.model.dart @@ -8,13 +8,7 @@ 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 -} +enum BackUpProgressEnum { idle, inProgress, manualInProgress, inBackground, done } class BackUpState { // enum @@ -105,26 +99,21 @@ class BackUpState { progressInFileSize: progressInFileSize ?? this.progressInFileSize, progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, - progressInFileSpeedUpdateTime: - progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, - progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? - this.progressInFileSpeedUpdateSentBytes, - iCloudDownloadProgress: - iCloudDownloadProgress ?? this.iCloudDownloadProgress, + progressInFileSpeedUpdateTime: progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, + progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? this.progressInFileSpeedUpdateSentBytes, + iCloudDownloadProgress: iCloudDownloadProgress ?? this.iCloudDownloadProgress, cancelToken: cancelToken ?? this.cancelToken, serverInfo: serverInfo ?? this.serverInfo, autoBackup: autoBackup ?? this.autoBackup, backgroundBackup: backgroundBackup ?? this.backgroundBackup, backupRequireWifi: backupRequireWifi ?? this.backupRequireWifi, - backupRequireCharging: - backupRequireCharging ?? this.backupRequireCharging, + 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, + selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds, currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, ); } @@ -146,8 +135,7 @@ class BackUpState { other.progressInFileSpeed == progressInFileSpeed && collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && - other.progressInFileSpeedUpdateSentBytes == - progressInFileSpeedUpdateSentBytes && + other.progressInFileSpeedUpdateSentBytes == progressInFileSpeedUpdateSentBytes && other.iCloudDownloadProgress == iCloudDownloadProgress && other.cancelToken == cancelToken && other.serverInfo == serverInfo && diff --git a/mobile/lib/models/backup/current_upload_asset.model.dart b/mobile/lib/models/backup/current_upload_asset.model.dart index 787f117269..2214897357 100644 --- a/mobile/lib/models/backup/current_upload_asset.model.dart +++ b/mobile/lib/models/backup/current_upload_asset.model.dart @@ -9,7 +9,7 @@ class CurrentUploadAsset { final int? fileSize; final bool? iCloudAsset; - CurrentUploadAsset({ + const CurrentUploadAsset({ required this.id, required this.fileCreatedAt, required this.fileName, @@ -53,13 +53,11 @@ class CurrentUploadAsset { factory CurrentUploadAsset.fromMap(Map map) { return CurrentUploadAsset( id: map['id'] as String, - fileCreatedAt: - DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt'] as int), + 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, + iCloudAsset: map['iCloudAsset'] != null ? map['iCloudAsset'] as bool : null, ); } diff --git a/mobile/lib/models/backup/manual_upload_state.model.dart b/mobile/lib/models/backup/manual_upload_state.model.dart index a2d84fbef3..7f797334de 100644 --- a/mobile/lib/models/backup/manual_upload_state.model.dart +++ b/mobile/lib/models/backup/manual_upload_state.model.dart @@ -56,17 +56,14 @@ class ManualUploadState { progressInFileSize: progressInFileSize ?? this.progressInFileSize, progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, - progressInFileSpeedUpdateTime: - progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, - progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? - this.progressInFileSpeedUpdateSentBytes, + progressInFileSpeedUpdateTime: progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, + progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? this.progressInFileSpeedUpdateSentBytes, cancelToken: cancelToken ?? this.cancelToken, currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, totalAssetsToUpload: totalAssetsToUpload ?? this.totalAssetsToUpload, currentAssetIndex: currentAssetIndex ?? this.currentAssetIndex, successfulUploads: successfulUploads ?? this.successfulUploads, - showDetailedNotification: - showDetailedNotification ?? this.showDetailedNotification, + showDetailedNotification: showDetailedNotification ?? this.showDetailedNotification, ); } @@ -86,8 +83,7 @@ class ManualUploadState { other.progressInFileSpeed == progressInFileSpeed && collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && - other.progressInFileSpeedUpdateSentBytes == - progressInFileSpeedUpdateSentBytes && + other.progressInFileSpeedUpdateSentBytes == progressInFileSpeedUpdateSentBytes && other.cancelToken == cancelToken && other.currentUploadAsset == currentUploadAsset && other.totalAssetsToUpload == totalAssetsToUpload && diff --git a/mobile/lib/models/backup/success_upload_asset.model.dart b/mobile/lib/models/backup/success_upload_asset.model.dart index 045715e8cb..ca49450a31 100644 --- a/mobile/lib/models/backup/success_upload_asset.model.dart +++ b/mobile/lib/models/backup/success_upload_asset.model.dart @@ -5,7 +5,7 @@ class SuccessUploadAsset { final String remoteAssetId; final bool isDuplicate; - SuccessUploadAsset({ + const SuccessUploadAsset({ required this.candidate, required this.remoteAssetId, required this.isDuplicate, @@ -31,12 +31,9 @@ class SuccessUploadAsset { bool operator ==(covariant SuccessUploadAsset other) { if (identical(this, other)) return true; - return other.candidate == candidate && - other.remoteAssetId == remoteAssetId && - other.isDuplicate == isDuplicate; + return other.candidate == candidate && other.remoteAssetId == remoteAssetId && other.isDuplicate == isDuplicate; } @override - int get hashCode => - candidate.hashCode ^ remoteAssetId.hashCode ^ isDuplicate.hashCode; + int get hashCode => candidate.hashCode ^ remoteAssetId.hashCode ^ isDuplicate.hashCode; } diff --git a/mobile/lib/models/cast/cast_manager_state.dart b/mobile/lib/models/cast/cast_manager_state.dart index 703ceb4c47..c948921792 100644 --- a/mobile/lib/models/cast/cast_manager_state.dart +++ b/mobile/lib/models/cast/cast_manager_state.dart @@ -59,8 +59,7 @@ class CastManagerState { String toJson() => json.encode(toMap()); - factory CastManagerState.fromJson(String source) => - CastManagerState.fromMap(json.decode(source)); + factory CastManagerState.fromJson(String source) => CastManagerState.fromMap(json.decode(source)); @override String toString() => @@ -80,9 +79,5 @@ class CastManagerState { @override int get hashCode => - isCasting.hashCode ^ - receiverName.hashCode ^ - castState.hashCode ^ - currentTime.hashCode ^ - duration.hashCode; + isCasting.hashCode ^ receiverName.hashCode ^ castState.hashCode ^ currentTime.hashCode ^ duration.hashCode; } diff --git a/mobile/lib/models/download/download_state.model.dart b/mobile/lib/models/download/download_state.model.dart index edd2fa183e..b2bd389bc2 100644 --- a/mobile/lib/models/download/download_state.model.dart +++ b/mobile/lib/models/download/download_state.model.dart @@ -10,7 +10,7 @@ class DownloadInfo { // enum final TaskStatus status; - DownloadInfo({ + const DownloadInfo({ required this.fileName, required this.progress, required this.status, @@ -46,20 +46,16 @@ class DownloadInfo { String toJson() => json.encode(toMap()); - factory DownloadInfo.fromJson(String source) => - DownloadInfo.fromMap(json.decode(source) as Map); + factory DownloadInfo.fromJson(String source) => DownloadInfo.fromMap(json.decode(source) as Map); @override - String toString() => - 'DownloadInfo(fileName: $fileName, progress: $progress, status: $status)'; + String toString() => 'DownloadInfo(fileName: $fileName, progress: $progress, status: $status)'; @override bool operator ==(covariant DownloadInfo other) { if (identical(this, other)) return true; - return other.fileName == fileName && - other.progress == progress && - other.status == status; + return other.fileName == fileName && other.progress == progress && other.status == status; } @override @@ -71,7 +67,7 @@ class DownloadState { final TaskStatus downloadStatus; final Map taskProgress; final bool showProgress; - DownloadState({ + const DownloadState({ required this.downloadStatus, required this.taskProgress, required this.showProgress, @@ -104,6 +100,5 @@ class DownloadState { } @override - int get hashCode => - downloadStatus.hashCode ^ taskProgress.hashCode ^ showProgress.hashCode; + int get hashCode => downloadStatus.hashCode ^ taskProgress.hashCode ^ showProgress.hashCode; } diff --git a/mobile/lib/models/folder/recursive_folder.model.dart b/mobile/lib/models/folder/recursive_folder.model.dart index 5b54a2e1bf..62ec670fed 100644 --- a/mobile/lib/models/folder/recursive_folder.model.dart +++ b/mobile/lib/models/folder/recursive_folder.model.dart @@ -3,7 +3,7 @@ import 'package:immich_mobile/models/folder/root_folder.model.dart'; class RecursiveFolder extends RootFolder { final String name; - RecursiveFolder({ + const RecursiveFolder({ required this.name, required super.path, required super.subfolders, diff --git a/mobile/lib/models/folder/root_folder.model.dart b/mobile/lib/models/folder/root_folder.model.dart index 8f72a539c0..567093ecd5 100644 --- a/mobile/lib/models/folder/root_folder.model.dart +++ b/mobile/lib/models/folder/root_folder.model.dart @@ -4,7 +4,7 @@ class RootFolder { final List subfolders; final String path; - RootFolder({ + const RootFolder({ required this.subfolders, required this.path, }); diff --git a/mobile/lib/models/map/map_event.model.dart b/mobile/lib/models/map/map_event.model.dart index dd9fec06e6..a57fcb4c36 100644 --- a/mobile/lib/models/map/map_event.model.dart +++ b/mobile/lib/models/map/map_event.model.dart @@ -8,4 +8,6 @@ class MapAssetsInBoundsUpdated extends MapEvent { const MapAssetsInBoundsUpdated(this.assetRemoteIds); } -class MapCloseBottomSheet extends MapEvent {} +class MapCloseBottomSheet extends MapEvent { + const MapCloseBottomSheet(); +} diff --git a/mobile/lib/models/map/map_marker.model.dart b/mobile/lib/models/map/map_marker.model.dart index c9253a37cc..781eae792f 100644 --- a/mobile/lib/models/map/map_marker.model.dart +++ b/mobile/lib/models/map/map_marker.model.dart @@ -4,7 +4,7 @@ import 'package:openapi/api.dart'; class MapMarker { final LatLng latLng; final String assetRemoteId; - MapMarker({ + const MapMarker({ required this.latLng, required this.assetRemoteId, }); @@ -24,8 +24,7 @@ class MapMarker { assetRemoteId = dto.id; @override - String toString() => - 'MapMarker(latLng: $latLng, assetRemoteId: $assetRemoteId)'; + String toString() => 'MapMarker(latLng: $latLng, assetRemoteId: $assetRemoteId)'; @override bool operator ==(covariant MapMarker other) { diff --git a/mobile/lib/models/map/map_state.model.dart b/mobile/lib/models/map/map_state.model.dart index 973b925aa8..78747e770d 100644 --- a/mobile/lib/models/map/map_state.model.dart +++ b/mobile/lib/models/map/map_state.model.dart @@ -11,7 +11,7 @@ class MapState { final AsyncValue lightStyleFetched; final AsyncValue darkStyleFetched; - MapState({ + const MapState({ this.themeMode = ThemeMode.system, this.showFavoriteOnly = false, this.includeArchived = false, diff --git a/mobile/lib/models/memories/memory.model.dart b/mobile/lib/models/memories/memory.model.dart index 34691d3b55..fb85d70b9e 100644 --- a/mobile/lib/models/memories/memory.model.dart +++ b/mobile/lib/models/memories/memory.model.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/entities/asset.entity.dart'; class Memory { final String title; final List assets; - Memory({ + const Memory({ required this.title, required this.assets, }); @@ -30,9 +30,7 @@ class Memory { if (identical(this, other)) return true; final listEquals = const DeepCollectionEquality().equals; - return other is Memory && - other.title == title && - listEquals(other.assets, assets); + return other is Memory && other.title == title && listEquals(other.assets, assets); } @override diff --git a/mobile/lib/models/search/search_curated_content.model.dart b/mobile/lib/models/search/search_curated_content.model.dart index a3d74941b3..7ecb5af45c 100644 --- a/mobile/lib/models/search/search_curated_content.model.dart +++ b/mobile/lib/models/search/search_curated_content.model.dart @@ -14,7 +14,7 @@ class SearchCuratedContent { /// The id to lookup the asset from the server final String id; - SearchCuratedContent({ + const SearchCuratedContent({ required this.label, required this.id, this.subtitle, @@ -54,8 +54,7 @@ class SearchCuratedContent { SearchCuratedContent.fromMap(json.decode(source) as Map); @override - String toString() => - 'CuratedContent(label: $label, subtitle: $subtitle, id: $id)'; + String toString() => 'CuratedContent(label: $label, subtitle: $subtitle, id: $id)'; @override bool operator ==(covariant SearchCuratedContent other) { diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 835e6aff8f..1c2167faac 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -48,16 +48,13 @@ class SearchLocationFilter { SearchLocationFilter.fromMap(json.decode(source) as Map); @override - String toString() => - 'SearchLocationFilter(country: $country, state: $state, city: $city)'; + String toString() => 'SearchLocationFilter(country: $country, state: $state, city: $city)'; @override bool operator ==(covariant SearchLocationFilter other) { if (identical(this, other)) return true; - return other.country == country && - other.state == state && - other.city == city; + return other.country == country && other.state == state && other.city == city; } @override @@ -142,12 +139,8 @@ class SearchDateFilter { factory SearchDateFilter.fromMap(Map map) { return SearchDateFilter( - takenBefore: map['takenBefore'] != null - ? DateTime.fromMillisecondsSinceEpoch(map['takenBefore'] as int) - : null, - takenAfter: map['takenAfter'] != null - ? DateTime.fromMillisecondsSinceEpoch(map['takenAfter'] as int) - : null, + takenBefore: map['takenBefore'] != null ? DateTime.fromMillisecondsSinceEpoch(map['takenBefore'] as int) : null, + takenAfter: map['takenAfter'] != null ? DateTime.fromMillisecondsSinceEpoch(map['takenAfter'] as int) : null, ); } @@ -157,8 +150,7 @@ class SearchDateFilter { SearchDateFilter.fromMap(json.decode(source) as Map); @override - String toString() => - 'SearchDateFilter(takenBefore: $takenBefore, takenAfter: $takenAfter)'; + String toString() => 'SearchDateFilter(takenBefore: $takenBefore, takenAfter: $takenAfter)'; @override bool operator ==(covariant SearchDateFilter other) { @@ -222,14 +214,11 @@ class SearchDisplayFilters { bool operator ==(covariant SearchDisplayFilters other) { if (identical(this, other)) return true; - return other.isNotInAlbum == isNotInAlbum && - other.isArchive == isArchive && - other.isFavorite == isFavorite; + return other.isNotInAlbum == isNotInAlbum && other.isArchive == isArchive && other.isFavorite == isFavorite; } @override - int get hashCode => - isNotInAlbum.hashCode ^ isArchive.hashCode ^ isFavorite.hashCode; + int get hashCode => isNotInAlbum.hashCode ^ isArchive.hashCode ^ isFavorite.hashCode; } class SearchFilter { @@ -237,7 +226,7 @@ class SearchFilter { String? filename; String? description; String? language; - Set people; + Set people; SearchLocationFilter location; SearchCameraFilter camera; SearchDateFilter date; @@ -282,7 +271,7 @@ class SearchFilter { String? filename, String? description, String? language, - Set? people, + Set? people, SearchLocationFilter? location, SearchCameraFilter? camera, SearchDateFilter? date, diff --git a/mobile/lib/models/search/search_result.model.dart b/mobile/lib/models/search/search_result.model.dart index f51353ad61..458a9b4abc 100644 --- a/mobile/lib/models/search/search_result.model.dart +++ b/mobile/lib/models/search/search_result.model.dart @@ -6,7 +6,7 @@ class SearchResult { final List assets; final int? nextPage; - SearchResult({ + const SearchResult({ required this.assets, this.nextPage, }); diff --git a/mobile/lib/models/search/search_result_page_state.model.dart b/mobile/lib/models/search/search_result_page_state.model.dart index 00895c4586..7c8a27b50c 100644 --- a/mobile/lib/models/search/search_result_page_state.model.dart +++ b/mobile/lib/models/search/search_result_page_state.model.dart @@ -8,7 +8,7 @@ class SearchResultPageState { final bool isSmart; final List searchResult; - SearchResultPageState({ + const SearchResultPageState({ required this.isLoading, required this.isSuccess, required this.isError, @@ -52,10 +52,6 @@ class SearchResultPageState { @override int get hashCode { - return isLoading.hashCode ^ - isSuccess.hashCode ^ - isError.hashCode ^ - isSmart.hashCode ^ - searchResult.hashCode; + return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ isSmart.hashCode ^ searchResult.hashCode; } } diff --git a/mobile/lib/models/server_info/server_config.model.dart b/mobile/lib/models/server_info/server_config.model.dart index f07ffde522..88c27443c4 100644 --- a/mobile/lib/models/server_info/server_config.model.dart +++ b/mobile/lib/models/server_info/server_config.model.dart @@ -50,6 +50,5 @@ class ServerConfig { } @override - int get hashCode => - trashDays.hashCode ^ oauthButtonText.hashCode ^ externalDomain.hashCode; + int get hashCode => trashDays.hashCode ^ oauthButtonText.hashCode ^ externalDomain.hashCode; } diff --git a/mobile/lib/models/server_info/server_disk_info.model.dart b/mobile/lib/models/server_info/server_disk_info.model.dart index 01ce49beec..8248097ca5 100644 --- a/mobile/lib/models/server_info/server_disk_info.model.dart +++ b/mobile/lib/models/server_info/server_disk_info.model.dart @@ -51,9 +51,6 @@ class ServerDiskInfo { @override int get hashCode { - return diskAvailable.hashCode ^ - diskSize.hashCode ^ - diskUse.hashCode ^ - diskUsagePercentage.hashCode; + return diskAvailable.hashCode ^ diskSize.hashCode ^ diskUse.hashCode ^ diskUsagePercentage.hashCode; } } diff --git a/mobile/lib/models/server_info/server_features.model.dart b/mobile/lib/models/server_info/server_features.model.dart index fee88869ed..7e537ebf34 100644 --- a/mobile/lib/models/server_info/server_features.model.dart +++ b/mobile/lib/models/server_info/server_features.model.dart @@ -50,9 +50,6 @@ class ServerFeatures { @override int get hashCode { - return trash.hashCode ^ - map.hashCode ^ - oauthEnabled.hashCode ^ - passwordLogin.hashCode; + return trash.hashCode ^ map.hashCode ^ oauthEnabled.hashCode ^ passwordLogin.hashCode; } } diff --git a/mobile/lib/models/server_info/server_info.model.dart b/mobile/lib/models/server_info/server_info.model.dart index 8a70e13883..0fa80d45d8 100644 --- a/mobile/lib/models/server_info/server_info.model.dart +++ b/mobile/lib/models/server_info/server_info.model.dart @@ -13,7 +13,7 @@ class ServerInfo { final bool isNewReleaseAvailable; final String versionMismatchErrorMessage; - ServerInfo({ + const ServerInfo({ required this.serverVersion, required this.latestVersion, required this.serverFeatures, @@ -41,10 +41,8 @@ class ServerInfo { serverConfig: serverConfig ?? this.serverConfig, serverDiskInfo: serverDiskInfo ?? this.serverDiskInfo, isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch, - isNewReleaseAvailable: - isNewReleaseAvailable ?? this.isNewReleaseAvailable, - versionMismatchErrorMessage: - versionMismatchErrorMessage ?? this.versionMismatchErrorMessage, + isNewReleaseAvailable: isNewReleaseAvailable ?? this.isNewReleaseAvailable, + versionMismatchErrorMessage: versionMismatchErrorMessage ?? this.versionMismatchErrorMessage, ); } diff --git a/mobile/lib/models/server_info/server_version.model.dart b/mobile/lib/models/server_info/server_version.model.dart index 1995edb98d..bd71536622 100644 --- a/mobile/lib/models/server_info/server_version.model.dart +++ b/mobile/lib/models/server_info/server_version.model.dart @@ -37,10 +37,7 @@ class ServerVersion { bool operator ==(Object other) { if (identical(this, other)) return true; - return other is ServerVersion && - other.major == major && - other.minor == minor && - other.patch == patch; + return other is ServerVersion && other.major == major && other.minor == minor && other.patch == patch; } @override diff --git a/mobile/lib/models/shared_link/shared_link.model.dart b/mobile/lib/models/shared_link/shared_link.model.dart index a107dd892a..135d191b20 100644 --- a/mobile/lib/models/shared_link/shared_link.model.dart +++ b/mobile/lib/models/shared_link/shared_link.model.dart @@ -66,9 +66,7 @@ class SharedLink { expiresAt = dto.expiresAt, key = dto.key, showMetadata = dto.showMetadata, - type = dto.type == SharedLinkType.ALBUM - ? SharedLinkSource.album - : SharedLinkSource.individual, + type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual, title = dto.type == SharedLinkType.ALBUM ? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE" : "INDIVIDUAL SHARE", diff --git a/mobile/lib/models/upload/share_intent_attachment.model.dart b/mobile/lib/models/upload/share_intent_attachment.model.dart index 1bdb5b6b48..61157f3674 100644 --- a/mobile/lib/models/upload/share_intent_attachment.model.dart +++ b/mobile/lib/models/upload/share_intent_attachment.model.dart @@ -17,7 +17,7 @@ enum UploadStatus { notFound, failed, canceled, - waitingtoRetry, + waitingToRetry, paused, } @@ -90,8 +90,7 @@ class ShareIntentAttachment { String toJson() => json.encode(toMap()); - factory ShareIntentAttachment.fromJson(String source) => - ShareIntentAttachment.fromMap( + factory ShareIntentAttachment.fromJson(String source) => ShareIntentAttachment.fromMap( json.decode(source) as Map, ); 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 index 62406d2e3a..c0fc8c9364 100644 --- a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart +++ b/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart @@ -21,8 +21,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final AsyncValue> suggestedShareUsers = - ref.watch(otherUsersProvider); + final AsyncValue> suggestedShareUsers = ref.watch(otherUsersProvider); final sharedUsersList = useState>({}); addNewUsersHandler() { @@ -138,8 +137,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget { ), actions: [ TextButton( - onPressed: - sharedUsersList.value.isEmpty ? null : addNewUsersHandler, + onPressed: sharedUsersList.value.isEmpty ? null : addNewUsersHandler, child: const Text( "add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), diff --git a/mobile/lib/pages/album/album_asset_selection.page.dart b/mobile/lib/pages/album/album_asset_selection.page.dart index 7b0ce8cdc4..e72ecb0712 100644 --- a/mobile/lib/pages/album/album_asset_selection.page.dart +++ b/mobile/lib/pages/album/album_asset_selection.page.dart @@ -65,10 +65,8 @@ class AlbumAssetSelectionPage extends HookConsumerWidget { if (selected.value.isNotEmpty || canDeselect) TextButton( onPressed: () { - var payload = - AssetSelectionPageResult(selectedAssets: selected.value); - AutoRouter.of(context) - .popForced(payload); + var payload = AssetSelectionPageResult(selectedAssets: selected.value); + AutoRouter.of(context).popForced(payload); }, child: Text( canDeselect ? "done" : "add", diff --git a/mobile/lib/pages/album/album_control_button.dart b/mobile/lib/pages/album/album_control_button.dart index b2100946e6..c453ace618 100644 --- a/mobile/lib/pages/album/album_control_button.dart +++ b/mobile/lib/pages/album/album_control_button.dart @@ -1,52 +1,40 @@ 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/current_album.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; -// ignore: must_be_immutable class AlbumControlButton extends ConsumerWidget { - void Function() onAddPhotosPressed; - void Function() onAddUsersPressed; + final void Function()? onAddPhotosPressed; + final void Function()? onAddUsersPressed; - AlbumControlButton({ + const AlbumControlButton({ super.key, - required this.onAddPhotosPressed, - required this.onAddUsersPressed, + this.onAddPhotosPressed, + this.onAddUsersPressed, }); @override Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authProvider).userId; - final isOwner = ref.watch( - currentAlbumProvider.select((album) { - return album?.ownerId == userId; - }), - ); - - return Padding( - padding: const EdgeInsets.only(left: 16.0), - child: SizedBox( - height: 36, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ + 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 (isOwner) - AlbumActionFilledButton( - key: const ValueKey('add_users_button'), - iconData: Icons.person_add_alt_rounded, - onPressed: onAddUsersPressed, - labelText: "album_viewer_page_share_add_users".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 index 591be260f6..57d808f226 100644 --- a/mobile/lib/pages/album/album_date_range.dart +++ b/mobile/lib/pages/album/album_date_range.dart @@ -42,16 +42,12 @@ class AlbumDateRange extends ConsumerWidget { @pragma('vm:prefer-inline') String _getDateRangeText(DateTime startDate, DateTime endDate) { - if (startDate.day == endDate.day && - startDate.month == endDate.month && - startDate.year == endDate.year) { + 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 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_options.page.dart b/mobile/lib/pages/album/album_options.page.dart index f177686128..4c51093345 100644 --- a/mobile/lib/pages/album/album_options.page.dart +++ b/mobile/lib/pages/album/album_options.page.dart @@ -7,8 +7,7 @@ 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/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'; @@ -28,8 +27,7 @@ class AlbumOptionsPage extends HookConsumerWidget { return const SizedBox(); } - final sharedUsers = - useState(album.sharedUsers.map((u) => u.toDto()).toList()); + 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); @@ -50,8 +48,7 @@ class AlbumOptionsPage extends HookConsumerWidget { isProcessing.value = true; try { - final isSuccess = - await ref.read(albumProvider.notifier).leaveAlbum(album); + final isSuccess = await ref.read(albumProvider.notifier).leaveAlbum(album); if (isSuccess) { context.navigateTo( @@ -99,8 +96,7 @@ class AlbumOptionsPage extends HookConsumerWidget { actions = [ ListTile( leading: const Icon(Icons.person_remove_rounded), - title: const Text("shared_album_section_people_action_remove_user") - .tr(), + title: const Text("shared_album_section_people_action_remove_user").tr(), onTap: () => removeUserFromAlbum(user), ), ]; @@ -126,9 +122,7 @@ class AlbumOptionsPage extends HookConsumerWidget { buildOwnerInfo() { return ListTile( - leading: owner != null - ? UserCircleAvatar(user: owner.toDto()) - : const SizedBox(), + leading: owner != null ? UserCircleAvatar(user: owner.toDto()) : const SizedBox(), title: Text( album.owner.value?.name ?? "", style: const TextStyle( @@ -170,12 +164,8 @@ class AlbumOptionsPage extends HookConsumerWidget { 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, + trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(), + onTap: userId == user.id || isOwner ? () => handleUserClick(user) : null, ); }, ); @@ -204,20 +194,15 @@ class AlbumOptionsPage extends HookConsumerWidget { value: activityEnabled.value, onChanged: (bool value) async { activityEnabled.value = value; - if (await ref - .read(albumProvider.notifier) - .setActivitystatus(album, value)) { + if (await ref.read(albumProvider.notifier).setActivitystatus(album, value)) { album.activityEnabled = value; } }, - activeColor: activityEnabled.value - ? context.primaryColor - : context.themeData.disabledColor, + activeColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, dense: true, title: Text( "comments_and_likes", - style: context.textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.w500), + style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), ).tr(), subtitle: Text( "let_others_respond", diff --git a/mobile/lib/pages/album/album_viewer.dart b/mobile/lib/pages/album/album_viewer.dart index 86b23fba30..6d9519f4ed 100644 --- a/mobile/lib/pages/album/album_viewer.dart +++ b/mobile/lib/pages/album/album_viewer.dart @@ -41,10 +41,14 @@ class AlbumViewer extends HookConsumerWidget { 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); + final bool isSuccess = await ref.read(albumProvider.notifier).removeAsset(album, assets); if (!isSuccess) { ImmichToast.show( @@ -60,8 +64,7 @@ class AlbumViewer extends HookConsumerWidget { /// 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( + AssetSelectionPageResult? returnPayload = await context.pushRoute( AlbumAssetSelectionRoute( existingAssets: album.assets, canDeselect: false, @@ -72,9 +75,7 @@ class AlbumViewer extends HookConsumerWidget { // Check if there is new assets add isProcessing.value = true; - await ref - .watch(albumProvider.notifier) - .addAssets(album, returnPayload.selectedAssets); + await ref.watch(albumProvider.notifier).addAssets(album, returnPayload.selectedAssets); isProcessing.value = false; } @@ -138,10 +139,13 @@ class AlbumViewer extends HookConsumerWidget { ), const AlbumSharedUserIcons(), if (album.isRemote) - AlbumControlButton( - key: const ValueKey("albumControlButton"), - onAddPhotosPressed: onAddPhotosPressed, - onAddUsersPressed: onAddUsersPressed, + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: AlbumControlButton( + key: const ValueKey("albumControlButton"), + onAddPhotosPressed: onAddPhotosPressed, + onAddUsersPressed: isOwner ? onAddUsersPressed : null, + ), ), const SizedBox(height: 8), ], diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart index 2a13ccccd7..aea4cfa2b8 100644 --- a/mobile/lib/pages/albums/albums.page.dart +++ b/mobile/lib/pages/albums/albums.page.dart @@ -26,8 +26,7 @@ class AlbumsPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final albums = - ref.watch(albumProvider).where((album) => album.isRemote).toList(); + 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); @@ -105,7 +104,9 @@ class AlbumsPage extends HookConsumerWidget { color: context.colorScheme.onSurface.withAlpha(0), width: 0, ), - borderRadius: BorderRadius.circular(24), + borderRadius: const BorderRadius.all( + Radius.circular(24), + ), gradient: LinearGradient( colors: [ context.colorScheme.primary.withValues(alpha: 0.075), @@ -129,8 +130,7 @@ class AlbumsPage extends HookConsumerWidget { ) : null, controller: searchController, - onChanged: (_) => - onSearch(searchController.text, filterMode.value), + onChanged: (_) => onSearch(searchController.text, filterMode.value), focusNode: searchFocusNode, onTapOutside: (_) => searchFocusNode.unfocus(), ), @@ -178,9 +178,7 @@ class AlbumsPage extends HookConsumerWidget { const SortButton(), IconButton( icon: Icon( - isGrid.value - ? Icons.view_list_outlined - : Icons.grid_view_outlined, + isGrid.value ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24, ), onPressed: toggleViewMode, @@ -194,8 +192,7 @@ class AlbumsPage extends HookConsumerWidget { ? GridView.builder( shrinkWrap: true, physics: const ClampingScrollPhysics(), - gridDelegate: - const SliverGridDelegateWithMaxCrossAxisExtent( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 250, mainAxisSpacing: 12, crossAxisSpacing: 12, @@ -242,10 +239,8 @@ class AlbumsPage extends HookConsumerWidget { }, ) : 'owned'.t(context: context)}', overflow: TextOverflow.ellipsis, - style: - context.textTheme.bodyMedium?.copyWith( - color: context - .colorScheme.onSurfaceSecondary, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, ), ) : null, @@ -301,7 +296,9 @@ class QuickFilterButton extends StatelessWidget { ), shape: WidgetStateProperty.all( RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), side: BorderSide( color: context.colorScheme.onSurface.withAlpha(25), width: 1, @@ -312,9 +309,7 @@ class QuickFilterButton extends StatelessWidget { child: Text( label, style: TextStyle( - color: isSelected - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface, + color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface, fontSize: 14, ), ), @@ -334,8 +329,10 @@ class SortButton extends ConsumerWidget { style: MenuStyle( elevation: const WidgetStatePropertyAll(1), shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), + const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(24), + ), ), ), padding: const WidgetStatePropertyAll( @@ -350,28 +347,22 @@ class SortButton extends ConsumerWidget { ? albumSortIsReverse ? Icon( Icons.keyboard_arrow_down, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface, + color: + albumSortOption == mode ? context.colorScheme.onPrimary : context.colorScheme.onSurface, ) : Icon( Icons.keyboard_arrow_up_rounded, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface, + 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); + ref.read(albumSortOrderProvider.notifier).changeSortDirection(!albumSortIsReverse); } else { - ref - .read(albumSortByOptionsProvider.notifier) - .changeSortMode(mode); + ref.read(albumSortByOptionsProvider.notifier).changeSortMode(mode); } }, style: ButtonStyle( @@ -379,13 +370,13 @@ class SortButton extends ConsumerWidget { const EdgeInsets.fromLTRB(16, 16, 32, 16), ), backgroundColor: WidgetStateProperty.all( - albumSortOption == mode - ? context.colorScheme.primary - : Colors.transparent, + albumSortOption == mode ? context.colorScheme.primary : Colors.transparent, ), shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), + const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(24), + ), ), ), ), diff --git a/mobile/lib/pages/backup/album_preview.page.dart b/mobile/lib/pages/backup/album_preview.page.dart index b9fed41305..4cdc180973 100644 --- a/mobile/lib/pages/backup/album_preview.page.dart +++ b/mobile/lib/pages/backup/album_preview.page.dart @@ -19,9 +19,7 @@ class AlbumPreviewPage extends HookConsumerWidget { final assets = useState>([]); getAssetsInAlbum() async { - assets.value = await ref - .read(albumMediaRepositoryProvider) - .getAssets(album.localId!); + assets.value = await ref.read(albumMediaRepositoryProvider).getAssets(album.localId!); } useEffect( diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index c4124efb52..69e118cb78 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -19,8 +19,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; - final enableSyncUploadAlbum = - useAppSettingsState(AppSettingsEnum.syncAlbums); + final enableSyncUploadAlbum = useAppSettingsState(AppSettingsEnum.syncAlbums); final isDarkTheme = context.isDarkTheme; final albums = ref.watch(backupProvider).availableAlbums; @@ -85,8 +84,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { buildSelectedAlbumNameChip() { return selectedBackupAlbums.map((album) { - void removeSelection() => - ref.read(backupProvider.notifier).removeAlbumForBackup(album); + void removeSelection() => ref.read(backupProvider.notifier).removeAlbumForBackup(album); return Padding( padding: const EdgeInsets.only(right: 8.0), @@ -117,9 +115,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { buildExcludedAlbumNameChip() { return excludedBackupAlbums.map((album) { void removeSelection() { - ref - .watch(backupProvider.notifier) - .removeExcludedAlbumForBackup(album); + ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album); } return GestureDetector( @@ -215,11 +211,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { title: Text( "backup_album_selection_page_albums_device".tr( namedArgs: { - 'count': ref - .watch(backupProvider) - .availableAlbums - .length - .toString(), + 'count': ref.watch(backupProvider).availableAlbums.length.toString(), }, ), style: context.textTheme.titleSmall, @@ -246,8 +238,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { context: context, builder: (BuildContext context) { return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), ), elevation: 5, title: Text( diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart index 6cbf172ce5..76a772884e 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -30,11 +30,8 @@ class BackupControllerPage extends HookConsumerWidget { final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty; final didGetBackupInfo = useState(false); - bool hasExclusiveAccess = - backupState.backupProgress != BackUpProgressEnum.inBackground; - bool shouldBackup = backupState.allUniqueAssets.length - - backupState.selectedAlbumsBackupAssetsIds.length == - 0 || + bool hasExclusiveAccess = backupState.backupProgress != BackUpProgressEnum.inBackground; + bool shouldBackup = backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length == 0 || !hasExclusiveAccess ? false : true; @@ -48,9 +45,7 @@ class BackupControllerPage extends HookConsumerWidget { ref.watch(iOSBackgroundSettingsProvider.notifier).refresh(); } - ref - .watch(websocketProvider.notifier) - .stopListenToEvent('on_upload_success'); + ref.watch(websocketProvider.notifier).stopListenToEvent('on_upload_success'); return () { WakelockPlus.disable(); @@ -61,8 +56,7 @@ class BackupControllerPage extends HookConsumerWidget { useEffect( () { - if (backupState.backupProgress == BackUpProgressEnum.idle && - !didGetBackupInfo.value) { + if (backupState.backupProgress == BackUpProgressEnum.idle && !didGetBackupInfo.value) { ref.watch(backupProvider.notifier).getBackupInfo(); didGetBackupInfo.value = true; } @@ -147,7 +141,9 @@ class BackupControllerPage extends HookConsumerWidget { padding: const EdgeInsets.only(top: 8.0), child: Card( shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), side: BorderSide( color: context.colorScheme.outlineVariant, width: 1, @@ -181,9 +177,7 @@ class BackupControllerPage extends HookConsumerWidget { onPressed: () async { await context.pushRoute(const BackupAlbumSelectionRoute()); // waited until returning from selection - await ref - .read(backupProvider.notifier) - .backupAlbumSelectionDone(); + await ref.read(backupProvider.notifier).backupAlbumSelectionDone(); // waited until backup albums are stored in DB ref.read(albumProvider.notifier).refreshDeviceAlbums(); }, @@ -201,8 +195,7 @@ class BackupControllerPage extends HookConsumerWidget { void startBackup() { ref.watch(errorBackupListProvider.notifier).empty(); - if (ref.watch(backupProvider).backupProgress != - BackUpProgressEnum.inBackground) { + if (ref.watch(backupProvider).backupProgress != BackUpProgressEnum.inBackground) { ref.watch(backupProvider.notifier).startBackupProcess(); } } @@ -214,8 +207,7 @@ class BackupControllerPage extends HookConsumerWidget { ), child: Container( child: backupState.backupProgress == BackUpProgressEnum.inProgress || - backupState.backupProgress == - BackUpProgressEnum.manualInProgress + backupState.backupProgress == BackUpProgressEnum.manualInProgress ? ElevatedButton( style: ElevatedButton.styleFrom( foregroundColor: Colors.grey[50], @@ -223,8 +215,7 @@ class BackupControllerPage extends HookConsumerWidget { // padding: const EdgeInsets.all(14), ), onPressed: () { - if (backupState.backupProgress == - BackUpProgressEnum.manualInProgress) { + if (backupState.backupProgress == BackUpProgressEnum.manualInProgress) { ref.read(manualUploadProvider.notifier).cancelBackup(); } else { ref.read(backupProvider.notifier).cancelBackup(); diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart new file mode 100644 index 0000000000..f691eb576b --- /dev/null +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -0,0 +1,281 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart'; +import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; + +@RoutePage() +class DriftBackupPage extends ConsumerStatefulWidget { + const DriftBackupPage({super.key}); + + @override + ConsumerState createState() => _DriftBackupPageState(); +} + +class _DriftBackupPageState extends ConsumerState { + @override + void initState() { + super.initState(); + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + } + + Future startBackup() async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id); + } + + Future stopBackup() async { + await ref.read(driftBackupProvider.notifier).cancel(); + } + + @override + Widget build(BuildContext context) { + final selectedAlbum = ref + .watch(backupAlbumProvider) + .where( + (album) => album.backupSelection == BackupSelection.selected, + ) + .toList(); + + return Scaffold( + appBar: AppBar( + elevation: 0, + title: Text( + "backup_controller_page_backup".t(), + ), + leading: IconButton( + onPressed: () { + context.maybePop(true); + }, + splashRadius: 24, + icon: const Icon( + Icons.arrow_back_ios_rounded, + ), + ), + ), + body: Stack( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16, + bottom: 32, + ), + child: ListView( + children: [ + const SizedBox(height: 8), + const _BackupAlbumSelectionCard(), + if (selectedAlbum.isNotEmpty) ...[ + const _TotalCard(), + const _BackupCard(), + const _RemainderCard(), + const Divider(), + BackupToggleButton( + onStart: () async => await startBackup(), + onStop: () async => await stopBackup(), + ), + TextButton.icon( + icon: const Icon(Icons.info_outline_rounded), + onPressed: () => context.pushRoute( + const DriftUploadDetailRoute(), + ), + label: Text("view_details".t(context: context)), + ), + ], + ], + ), + ), + ], + ), + ); + } +} + +class _BackupAlbumSelectionCard extends ConsumerWidget { + const _BackupAlbumSelectionCard(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + Widget buildSelectedAlbumName() { + String text = "backup_controller_page_backup_selected".tr(); + final albums = ref + .watch(backupAlbumProvider) + .where( + (album) => album.backupSelection == BackupSelection.selected, + ) + .toList(); + + 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() { + String text = "backup_controller_page_excluded".tr(); + final albums = ref + .watch(backupAlbumProvider) + .where( + (album) => album.backupSelection == BackupSelection.excluded, + ) + .toList(); + + 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(); + } + } + + return 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 DriftBackupAlbumSelectionRoute()); + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + }, + child: const Text( + "select", + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ).tr(), + ), + ), + ); + } +} + +class _TotalCard extends ConsumerWidget { + const _TotalCard(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final totalCount = ref.watch(driftBackupProvider.select((p) => p.totalCount)); + + return BackupInfoCard( + title: "total".tr(), + subtitle: "backup_controller_page_total_sub".tr(), + info: totalCount.toString(), + ); + } +} + +class _BackupCard extends ConsumerWidget { + const _BackupCard(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final backupCount = ref.watch(driftBackupProvider.select((p) => p.backupCount)); + + return BackupInfoCard( + title: "backup_controller_page_backup".tr(), + subtitle: "backup_controller_page_backup_sub".tr(), + info: backupCount.toString(), + ); + } +} + +class _RemainderCard extends ConsumerWidget { + const _RemainderCard(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final remainderCount = ref.watch(driftBackupProvider.select((p) => p.remainderCount)); + return BackupInfoCard( + title: "backup_controller_page_remainder".tr(), + subtitle: "backup_controller_page_remainder_sub".tr(), + info: remainderCount.toString(), + ); + } +} diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart new file mode 100644 index 0000000000..3f12328a06 --- /dev/null +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -0,0 +1,540 @@ +import 'dart:io'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart'; +import 'package:immich_mobile/widgets/common/search_field.dart'; +import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; + +@RoutePage() +class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget { + const DriftBackupAlbumSelectionPage({super.key}); + + @override + ConsumerState createState() => _DriftBackupAlbumSelectionPageState(); +} + +class _DriftBackupAlbumSelectionPageState extends ConsumerState { + String _searchQuery = ''; + bool _isSearchMode = false; + int _initialTotalAssetCount = 0; + bool _hasPopped = false; + late ValueNotifier _enableSyncUploadAlbum; + late TextEditingController _searchController; + late FocusNode _searchFocusNode; + + @override + void initState() { + super.initState(); + _enableSyncUploadAlbum = ValueNotifier(false); + _searchController = TextEditingController(); + _searchFocusNode = FocusNode(); + + _enableSyncUploadAlbum.value = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); + ref.read(backupAlbumProvider.notifier).getAll(); + + _initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount)); + } + + @override + void dispose() { + _enableSyncUploadAlbum.dispose(); + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final albums = ref.watch(backupAlbumProvider); + final albumCount = albums.length; + // Filter albums based on search query + final filteredAlbums = albums.where((album) { + if (_searchQuery.isEmpty) return true; + return album.name.toLowerCase().contains(_searchQuery.toLowerCase()); + }).toList(); + + final selectedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.selected).toList(); + final excludedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.excluded).toList(); + + handleSyncAlbumToggle(bool isEnable) async { + if (isEnable) { + await ref.read(albumProvider.notifier).refreshRemoteAlbums(); + for (final album in selectedBackupAlbums) { + await ref.read(albumProvider.notifier).createSyncAlbum(album.name); + } + } + } + + return PopScope( + onPopInvokedWithResult: (didPop, result) async { + // There is an issue with Flutter where the pop event + // can be triggered multiple times, so we guard it with _hasPopped + if (didPop && !_hasPopped) { + _hasPopped = true; + + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount)); + + if (currentTotalAssetCount != _initialTotalAssetCount) { + final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); + + if (!isBackupEnabled) { + return; + } + final backupNotifier = ref.read(driftBackupProvider.notifier); + + backupNotifier.cancel().then((_) { + backupNotifier.startBackup(currentUser.id); + }); + } + } + }, + child: Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () async => await context.maybePop(), + icon: const Icon(Icons.arrow_back_ios_rounded), + ), + title: _isSearchMode + ? SearchField( + hintText: 'search_albums'.t(context: context), + autofocus: true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) => setState(() => _searchQuery = value.trim()), + ) + : const Text( + "backup_album_selection_page_select_albums", + ).t(context: context), + actions: [ + if (!_isSearchMode) + IconButton( + icon: const Icon(Icons.search), + onPressed: () => setState(() { + _isSearchMode = true; + _searchQuery = ''; + }), + ) + else + IconButton( + icon: const Icon(Icons.close), + onPressed: () => setState(() { + _isSearchMode = false; + _searchQuery = ''; + _searchController.clear(); + }), + ), + ], + 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, + ).t(context: context), + ), + // Selected Album Chips + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Wrap( + children: [ + _SelectedAlbumNameChips( + selectedBackupAlbums: selectedBackupAlbums, + ), + _ExcludedAlbumNameChips( + excludedBackupAlbums: excludedBackupAlbums, + ), + ], + ), + ), + + SettingsSwitchListTile( + valueNotifier: _enableSyncUploadAlbum, + title: "sync_albums".t(context: context), + subtitle: "sync_upload_album_setting_subtitle".t(context: context), + 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( + "albums_on_device_count".t( + context: context, + args: {'count': albumCount.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, + ), + ).t(context: context), + ), + 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, + ), + ).t(context: context), + content: SingleChildScrollView( + child: ListBody( + children: [ + const Text( + 'backup_album_selection_page_assets_scatter', + style: TextStyle( + fontSize: 14, + ), + ).t(context: context), + ], + ), + ), + ); + }, + ); + }, + ), + ), + + if (Platform.isAndroid) + _SelectAllButton( + filteredAlbums: filteredAlbums, + selectedBackupAlbums: selectedBackupAlbums, + ), + ], + ), + ), + SliverLayoutBuilder( + builder: (context, constraints) { + if (constraints.crossAxisExtent > 600) { + return _AlbumSelectionGrid( + filteredAlbums: filteredAlbums, + searchQuery: _searchQuery, + ); + } else { + return _AlbumSelectionList( + filteredAlbums: filteredAlbums, + searchQuery: _searchQuery, + ); + } + }, + ), + ], + ), + ), + ); + } +} + +class _AlbumSelectionList extends StatelessWidget { + final List filteredAlbums; + final String searchQuery; + + const _AlbumSelectionList({ + required this.filteredAlbums, + required this.searchQuery, + }); + + @override + Widget build(BuildContext context) { + if (filteredAlbums.isEmpty && searchQuery.isNotEmpty) { + return SliverToBoxAdapter( + child: Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Text('album_search_not_found'.t(context: context)), + ), + ), + ); + } + + if (filteredAlbums.isEmpty) { + return const SliverToBoxAdapter( + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + + return SliverPadding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + ((context, index) { + return DriftAlbumInfoListTile( + album: filteredAlbums[index], + ); + }), + childCount: filteredAlbums.length, + ), + ), + ); + } +} + +class _AlbumSelectionGrid extends StatelessWidget { + final List filteredAlbums; + final String searchQuery; + + const _AlbumSelectionGrid({ + required this.filteredAlbums, + required this.searchQuery, + }); + + @override + Widget build(BuildContext context) { + if (filteredAlbums.isEmpty && searchQuery.isNotEmpty) { + return SliverToBoxAdapter( + child: Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Text('album_search_not_found'.t(context: context)), + ), + ), + ); + } + + if (filteredAlbums.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: filteredAlbums.length, + itemBuilder: ((context, index) { + return DriftAlbumInfoListTile( + album: filteredAlbums[index], + ); + }), + ), + ); + } +} + +class _SelectedAlbumNameChips extends ConsumerWidget { + final List selectedBackupAlbums; + + const _SelectedAlbumNameChips({ + required this.selectedBackupAlbums, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Wrap( + children: selectedBackupAlbums.asMap().entries.map((entry) { + final album = entry.value; + + void removeSelection() { + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + } + + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: GestureDetector( + onTap: removeSelection, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + child: Chip( + label: Text( + album.name, + style: TextStyle( + fontSize: 12, + color: context.isDarkTheme ? Colors.black : Colors.white, + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: context.primaryColor, + deleteIconColor: context.isDarkTheme ? Colors.black : Colors.white, + deleteIcon: const Icon( + Icons.cancel_rounded, + size: 15, + ), + onDeleted: removeSelection, + ), + ), + ), + ); + }).toList(), + ); + } +} + +class _ExcludedAlbumNameChips extends ConsumerWidget { + final List excludedBackupAlbums; + + const _ExcludedAlbumNameChips({ + required this.excludedBackupAlbums, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Wrap( + children: excludedBackupAlbums.asMap().entries.map((entry) { + final album = entry.value; + + void removeSelection() { + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + } + + return GestureDetector( + onTap: removeSelection, + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + 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, + ), + ), + ), + ); + }).toList(), + ); + } +} + +class _SelectAllButton extends ConsumerWidget { + final List filteredAlbums; + final List selectedBackupAlbums; + + const _SelectAllButton({ + required this.filteredAlbums, + required this.selectedBackupAlbums, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final canSelectAll = filteredAlbums.where((album) => album.backupSelection != BackupSelection.selected).isNotEmpty; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: canSelectAll + ? () { + for (final album in filteredAlbums) { + if (album.backupSelection != BackupSelection.selected) { + ref.read(backupAlbumProvider.notifier).selectAlbum(album); + } + } + } + : null, + icon: const Icon(Icons.select_all), + label: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Text( + "select_all".t(context: context), + ), + ), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12.0), + ), + ), + ), + const SizedBox(width: 8.0), + Expanded( + child: OutlinedButton.icon( + onPressed: selectedBackupAlbums.isNotEmpty + ? () { + for (final album in filteredAlbums) { + if (album.backupSelection == BackupSelection.selected) { + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + } + } + } + : null, + icon: const Icon(Icons.deselect), + label: Text('deselect_all'.t(context: context)), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12.0), + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/pages/backup/drift_upload_detail.page.dart b/mobile/lib/pages/backup/drift_upload_detail.page.dart new file mode 100644 index 0000000000..62d6341fd1 --- /dev/null +++ b/mobile/lib/pages/backup/drift_upload_detail.page.dart @@ -0,0 +1,419 @@ +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/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/utils/bytes_units.dart'; +import 'package:path/path.dart' as path; + +@RoutePage() +class DriftUploadDetailPage extends ConsumerWidget { + const DriftUploadDetailPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final uploadItems = ref.watch( + driftBackupProvider.select((state) => state.uploadItems), + ); + + return Scaffold( + appBar: AppBar( + title: Text("upload_details".t(context: context)), + backgroundColor: context.colorScheme.surface, + elevation: 0, + scrolledUnderElevation: 1, + ), + body: uploadItems.isEmpty ? _buildEmptyState(context) : _buildUploadList(uploadItems), + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.cloud_off_rounded, + size: 80, + color: context.colorScheme.onSurface.withValues(alpha: 0.3), + ), + const SizedBox(height: 16), + Text( + "no_uploads_in_progress".t(context: context), + style: context.textTheme.titleMedium?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ], + ), + ); + } + + Widget _buildUploadList( + Map uploadItems, + ) { + return ListView.separated( + addAutomaticKeepAlives: true, + padding: const EdgeInsets.all(16), + itemCount: uploadItems.length, + separatorBuilder: (context, index) => const SizedBox(height: 4), + itemBuilder: (context, index) { + final item = uploadItems.values.elementAt(index); + return _buildUploadCard(context, item); + }, + ); + } + + Widget _buildUploadCard( + BuildContext context, + DriftUploadStatus item, + ) { + final isCompleted = item.progress >= 1.0; + final double progressPercentage = (item.progress * 100).clamp(0, 100); + + return Card( + elevation: 0, + color: item.isFailed != null ? context.colorScheme.errorContainer : context.colorScheme.surfaceContainer, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all( + Radius.circular(16), + ), + side: BorderSide( + color: context.colorScheme.outline.withValues(alpha: 0.1), + width: 1, + ), + ), + child: InkWell( + onTap: () => _showFileDetailDialog(context, item), + borderRadius: const BorderRadius.all( + Radius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + path.basename(item.filename), + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + 'Tap for more details', + style: context.textTheme.bodySmall?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.6), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + _buildProgressIndicator( + context, + item.progress, + progressPercentage, + isCompleted, + item.networkSpeedAsString, + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildProgressIndicator( + BuildContext context, + double progress, + double percentage, + bool isCompleted, + String networkSpeedAsString, + ) { + return Column( + children: [ + Stack( + alignment: AlignmentDirectional.center, + children: [ + SizedBox( + width: 36, + height: 36, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: progress), + duration: const Duration(milliseconds: 300), + builder: (context, value, _) => CircularProgressIndicator( + backgroundColor: context.colorScheme.outline.withValues(alpha: 0.2), + strokeWidth: 3, + value: value, + color: isCompleted ? context.colorScheme.primary : context.colorScheme.secondary, + ), + ), + ), + if (isCompleted) + Icon( + Icons.check_circle_rounded, + size: 28, + color: context.colorScheme.primary, + ) + else + Text( + percentage.toStringAsFixed(0), + style: context.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.bold, + fontSize: 10, + ), + ), + ], + ), + Text( + networkSpeedAsString, + style: context.textTheme.labelSmall?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.6), + fontSize: 10, + ), + ), + ], + ); + } + + Future _showFileDetailDialog( + BuildContext context, + DriftUploadStatus item, + ) async { + showDialog( + context: context, + builder: (context) => FileDetailDialog(uploadStatus: item), + ); + } +} + +class FileDetailDialog extends ConsumerWidget { + final DriftUploadStatus uploadStatus; + + const FileDetailDialog({ + super.key, + required this.uploadStatus, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AlertDialog( + insetPadding: const EdgeInsets.all(20), + backgroundColor: context.colorScheme.surfaceContainerLow, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all( + Radius.circular(16), + ), + side: BorderSide( + color: context.colorScheme.outline.withValues(alpha: 0.2), + width: 1, + ), + ), + title: Row( + children: [ + Icon( + Icons.info_outline, + color: context.primaryColor, + size: 24, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + "details".t(context: context), + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ), + ), + ], + ), + content: SizedBox( + width: double.maxFinite, + child: FutureBuilder( + future: _getAssetDetails(ref, uploadStatus.taskId), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator()), + ); + } + + final asset = snapshot.data; + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Thumbnail at the top center + Center( + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Container( + width: 128, + height: 128, + decoration: BoxDecoration( + border: Border.all( + color: context.colorScheme.outline.withValues(alpha: 0.2), + width: 1, + ), + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + child: asset != null + ? Thumbnail( + asset: asset, + size: const Size(512, 512), + fit: BoxFit.cover, + ) + : null, + ), + ), + ), + const SizedBox(height: 24), + if (asset != null) ...[ + _buildInfoSection(context, [ + _buildInfoRow( + context, + "Filename", + path.basename(uploadStatus.filename), + ), + _buildInfoRow( + context, + "Local ID", + asset.id, + ), + _buildInfoRow( + context, + "File Size", + formatHumanReadableBytes(uploadStatus.fileSize, 2), + ), + if (asset.width != null) _buildInfoRow(context, "Width", "${asset.width}px"), + if (asset.height != null) + _buildInfoRow( + context, + "Height", + "${asset.height}px", + ), + _buildInfoRow( + context, + "Created At", + asset.createdAt.toString(), + ), + _buildInfoRow( + context, + "Updated At", + asset.updatedAt.toString(), + ), + if (asset.checksum != null) + _buildInfoRow( + context, + "Checksum", + asset.checksum!, + ), + ]), + ], + ], + ), + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + "close".t(), + style: TextStyle( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ), + ), + ], + ); + } + + Widget _buildInfoSection( + BuildContext context, + List children, + ) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainer, + borderRadius: const BorderRadius.all( + Radius.circular(12), + ), + border: Border.all( + color: context.colorScheme.outline.withValues(alpha: 0.1), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...children, + ], + ), + ); + } + + Widget _buildInfoRow(BuildContext context, String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 100, + child: Text( + "$label:", + style: context.textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.w500, + color: context.colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + ), + Expanded( + child: Text( + value, + style: context.textTheme.labelMedium?.copyWith(), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + Future _getAssetDetails( + WidgetRef ref, + String localAssetId, + ) async { + try { + final repository = ref.read(localAssetRepository); + return await repository.getById(localAssetId); + } catch (e) { + return null; + } + } +} diff --git a/mobile/lib/pages/backup/failed_backup_status.page.dart b/mobile/lib/pages/backup/failed_backup_status.page.dart index 551555d75e..8d0faf2d22 100644 --- a/mobile/lib/pages/backup/failed_backup_status.page.dart +++ b/mobile/lib/pages/backup/failed_backup_status.page.dart @@ -42,9 +42,11 @@ class FailedBackupStatusPage extends HookConsumerWidget { vertical: 4, ), child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), // if you need this - side: const BorderSide( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(15), // if you need this + ), + side: BorderSide( color: Colors.black12, width: 1, ), @@ -95,9 +97,7 @@ class FailedBackupStatusPage extends HookConsumerWidget { ), style: TextStyle( fontWeight: FontWeight.w600, - color: context.isDarkTheme - ? Colors.white70 - : Colors.grey[800], + color: context.isDarkTheme ? Colors.white70 : Colors.grey[800], ), ), Icon( @@ -123,9 +123,7 @@ class FailedBackupStatusPage extends HookConsumerWidget { errorAsset.errorMessage, style: TextStyle( fontWeight: FontWeight.w500, - color: context.isDarkTheme - ? Colors.white70 - : Colors.grey[800], + 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 index 776ee9977b..203df2d503 100644 --- a/mobile/lib/pages/common/activities.page.dart +++ b/mobile/lib/pages/common/activities.page.dart @@ -27,10 +27,8 @@ class ActivitiesPage extends HookConsumerWidget { 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 activityNotifier = ref.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); + final activities = ref.watch(albumActivityProvider(album.remoteId!, asset?.remoteId)); final listViewScrollController = useScrollController(); @@ -49,10 +47,7 @@ class ActivitiesPage extends HookConsumerWidget { body: activities.widgetWhen( onData: (data) { final liked = data.firstWhereOrNull( - (a) => - a.type == ActivityType.like && - a.user.id == user?.id && - a.assetId == asset?.remoteId, + (a) => a.type == ActivityType.like && a.user.id == user?.id && a.assetId == asset?.remoteId, ); return SafeArea( @@ -71,18 +66,15 @@ class ActivitiesPage extends HookConsumerWidget { } final activity = data[index]; - final canDelete = activity.user.id == user?.id || - album.ownerId == user?.id; + 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, + onDismiss: + canDelete ? (activityId) async => await activityNotifier.removeActivity(activity.id) : null, ), ); }, diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index 1bfea44ba1..d8647ca8e2 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.page.dart @@ -60,7 +60,9 @@ class AppLogDetailPage extends HookConsumerWidget { Container( decoration: BoxDecoration( color: context.colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(15.0), + borderRadius: const BorderRadius.all( + Radius.circular(15.0), + ), ), child: Padding( padding: const EdgeInsets.all(8.0), @@ -99,7 +101,9 @@ class AppLogDetailPage extends HookConsumerWidget { Container( decoration: BoxDecoration( color: context.colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(15.0), + borderRadius: const BorderRadius.all( + Radius.circular(15.0), + ), ), child: Padding( padding: const EdgeInsets.all(8.0), @@ -126,10 +130,8 @@ class AppLogDetailPage extends HookConsumerWidget { child: ListView( children: [ buildTextWithCopyButton("MESSAGE", logMessage.message), - if (logMessage.error != null) - buildTextWithCopyButton("DETAILS", logMessage.error.toString()), - if (logMessage.logger != null) - buildLogContext1(logMessage.logger.toString()), + if (logMessage.error != null) buildTextWithCopyButton("DETAILS", logMessage.error.toString()), + if (logMessage.logger != null) buildLogContext1(logMessage.logger.toString()), if (logMessage.stack != null) buildTextWithCopyButton( "STACK TRACE", diff --git a/mobile/lib/pages/common/change_experience.page.dart b/mobile/lib/pages/common/change_experience.page.dart new file mode 100644 index 0000000000..21464e39fa --- /dev/null +++ b/mobile/lib/pages/common/change_experience.page.dart @@ -0,0 +1,153 @@ +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/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/websocket.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/migration.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 { + bool hasMigrated = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _handleMigration()); + } + + Future _handleMigration() 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(); + + 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), + ); + } + } else { + await ref.read(backgroundSyncProvider).cancel(); + ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); + ref.read(websocketProvider.notifier).startListeningToOldEvents(); + } + + if (mounted) { + setState(() { + HapticFeedback.heavyImpact(); + hasMigrated = true; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedSwitcher( + duration: Durations.long4, + child: hasMigrated + ? const Icon( + Icons.check_circle_rounded, + color: Colors.green, + size: 48.0, + ) + : const SizedBox( + width: 50.0, + height: 50.0, + child: CircularProgressIndicator(), + ), + ), + const SizedBox(height: 16.0), + Center( + child: Column( + children: [ + SizedBox( + width: 300.0, + child: AnimatedSwitcher( + duration: Durations.long4, + child: hasMigrated + ? Text( + "Migration success!", + style: context.textTheme.titleMedium, + textAlign: TextAlign.center, + ) + : Text( + "Data migration in progress...\nPlease wait and don't close this page", + style: context.textTheme.titleMedium, + textAlign: TextAlign.center, + ), + ), + ), + if (hasMigrated) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: ElevatedButton( + onPressed: () { + context.replaceRoute( + widget.switchingToBeta ? const TabShellRoute() : const TabControllerRoute(), + ); + }, + child: const Text("Continue"), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart index f5c6321451..02e1ea18d4 100644 --- a/mobile/lib/pages/common/create_album.page.dart +++ b/mobile/lib/pages/common/create_album.page.dart @@ -27,8 +27,7 @@ class CreateAlbumPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final albumTitleController = - useTextEditingController.fromValue(TextEditingValue.empty); + final albumTitleController = useTextEditingController.fromValue(TextEditingValue.empty); final albumTitleTextFieldFocusNode = useFocusNode(); final albumDescriptionTextFieldFocusNode = useFocusNode(); final isAlbumTitleTextFieldFocus = useState(false); @@ -45,15 +44,12 @@ class CreateAlbumPage extends HookConsumerWidget { if (albumTitleController.text.isEmpty) { albumTitleController.text = 'create_album_page_untitled'.tr(); isAlbumTitleEmpty.value = false; - ref - .watch(albumTitleProvider.notifier) - .setAlbumTitle('create_album_page_untitled'.tr()); + ref.watch(albumTitleProvider.notifier).setAlbumTitle('create_album_page_untitled'.tr()); } } onSelectPhotosButtonPressed() async { - AssetSelectionPageResult? selectedAsset = - await context.pushRoute( + AssetSelectionPageResult? selectedAsset = await context.pushRoute( AlbumAssetSelectionRoute( existingAssets: selectedAssets.value, canDeselect: true, @@ -118,10 +114,11 @@ class CreateAlbumPage extends HookConsumerWidget { child: FilledButton.icon( style: FilledButton.styleFrom( alignment: Alignment.centerLeft, - padding: - const EdgeInsets.symmetric(vertical: 24, horizontal: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), ), backgroundColor: context.colorScheme.surfaceContainerHigh, ), @@ -228,15 +225,12 @@ class CreateAlbumPage extends HookConsumerWidget { ).tr(), actions: [ TextButton( - onPressed: - albumTitleController.text.isNotEmpty ? createAlbum : null, + onPressed: albumTitleController.text.isNotEmpty ? createAlbum : null, child: Text( 'create'.tr(), style: TextStyle( fontWeight: FontWeight.bold, - color: albumTitleController.text.isNotEmpty - ? context.primaryColor - : context.themeData.disabledColor, + color: albumTitleController.text.isNotEmpty ? context.primaryColor : context.themeData.disabledColor, ), ), ), diff --git a/mobile/lib/pages/common/download_panel.dart b/mobile/lib/pages/common/download_panel.dart index 5cc6e5b8d6..38212d5486 100644 --- a/mobile/lib/pages/common/download_panel.dart +++ b/mobile/lib/pages/common/download_panel.dart @@ -34,8 +34,7 @@ class DownloadPanel extends ConsumerWidget { duration: const Duration(milliseconds: 300), child: showProgress ? ConstrainedBox( - constraints: - BoxConstraints.loose(Size(context.width - 32, 300)), + constraints: BoxConstraints.loose(Size(context.width - 32, 300)), child: ListView.builder( shrinkWrap: true, itemCount: tasks.length, @@ -90,8 +89,10 @@ class DownloadTaskTile extends StatelessWidget { width: context.width - 32, child: Card( clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(16), + ), ), child: ListTile( minVerticalPadding: 18, @@ -120,8 +121,7 @@ class DownloadTaskTile extends StatelessWidget { child: LinearProgressIndicator( minHeight: 8.0, value: progress, - borderRadius: - const BorderRadius.all(Radius.circular(10.0)), + borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), ), const SizedBox(width: 8), diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 77b734ce0b..05389018da 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -125,7 +125,7 @@ class GalleryViewerPage extends HookConsumerWidget { final asset = loadAsset(currentIndex.value); if (asset.isRemote) { - ref.read(castProvider.notifier).loadMedia(asset, false); + ref.read(castProvider.notifier).loadMediaOld(asset, false); } else { if (isCasting) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -238,7 +238,7 @@ class GalleryViewerPage extends HookConsumerWidget { PhotoViewGalleryPageOptions buildImage(Asset asset) { return PhotoViewGalleryPageOptions( - onDragStart: (_, details, __) { + onDragStart: (_, details, __, ___) { localPosition.value = details.localPosition; }, onDragUpdate: (_, details, __) { @@ -267,8 +267,7 @@ class GalleryViewerPage extends HookConsumerWidget { PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) { return PhotoViewGalleryPageOptions.customChild( - onDragStart: (_, details, __) => - localPosition.value = details.localPosition, + onDragStart: (_, details, __, ___) => localPosition.value = details.localPosition, onDragUpdate: (_, details, __) => handleSwipeUpDown(details), heroAttributes: _getHeroAttributes(asset), filterQuality: FilterQuality.high, @@ -304,8 +303,7 @@ class GalleryViewerPage extends HookConsumerWidget { final stackId = newAsset.stackId; if (stackId != null && currentIndex.value == index) { - final stackElements = - ref.read(assetStackStateProvider(newAsset.stackId!)); + final stackElements = ref.read(assetStackStateProvider(newAsset.stackId!)); if (stackIndex.value < stackElements.length) { newAsset = stackElements.elementAt(stackIndex.value); } @@ -319,8 +317,7 @@ class GalleryViewerPage extends HookConsumerWidget { return PopScope( // Change immersive mode back to normal "edgeToEdge" mode - onPopInvokedWithResult: (didPop, _) => - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge), + onPopInvokedWithResult: (didPop, _) => SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge), child: Scaffold( backgroundColor: Colors.black, body: Stack( @@ -335,8 +332,7 @@ class GalleryViewerPage extends HookConsumerWidget { if (asset.isImage && !ref.read(isPlayingMotionVideoProvider)) { isZoomed.value = state != PhotoViewScaleState.initial; - ref.read(showControlsProvider.notifier).show = - !isZoomed.value; + ref.read(showControlsProvider.notifier).show = !isZoomed.value; } }, gaplessPlayback: true, @@ -370,7 +366,7 @@ class GalleryViewerPage extends HookConsumerWidget { ), itemCount: totalAssets.value, scrollDirection: Axis.horizontal, - onPageChanged: (value) { + onPageChanged: (value, _) { final next = currentIndex.value < value ? value + 1 : value - 1; ref.read(hapticFeedbackProvider.notifier).selectionClick(); @@ -394,7 +390,7 @@ class GalleryViewerPage extends HookConsumerWidget { // send image to casting if the server has it if (newAsset.isRemote) { - ref.read(castProvider.notifier).loadMedia(newAsset, false); + ref.read(castProvider.notifier).loadMediaOld(newAsset, false); } else { context.scaffoldMessenger.clearSnackBars(); @@ -454,9 +450,7 @@ class GalleryViewerPage extends HookConsumerWidget { @pragma('vm:prefer-inline') PhotoViewHeroAttributes _getHeroAttributes(Asset asset) { return PhotoViewHeroAttributes( - tag: asset.isInDb - ? asset.id + heroOffset - : '${asset.remoteId}-$heroOffset', + tag: asset.isInDb ? asset.id + heroOffset : '${asset.remoteId}-$heroOffset', transitionOnUserGestures: true, ); } diff --git a/mobile/lib/pages/common/large_leading_tile.dart b/mobile/lib/pages/common/large_leading_tile.dart index 4f22a5f2b2..d36e296429 100644 --- a/mobile/lib/pages/common/large_leading_tile.dart +++ b/mobile/lib/pages/common/large_leading_tile.dart @@ -40,8 +40,7 @@ class LargeLeadingTile extends StatelessWidget { child: Container( decoration: BoxDecoration( color: selected - ? selectedTileColor ?? - Theme.of(context).primaryColor.withAlpha(30) + ? selectedTileColor ?? Theme.of(context).primaryColor.withAlpha(30) : tileColor ?? Colors.transparent, borderRadius: BorderRadius.circular(borderRadius), ), diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart index 8afa6ab4e3..0dbaf6125c 100644 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ b/mobile/lib/pages/common/native_video_viewer.page.dart @@ -63,6 +63,8 @@ class NativeVideoViewerPage extends HookConsumerWidget { final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); + final isVideoReady = useState(false); + Future createSource() async { if (!context.mounted) { return null; @@ -85,11 +87,9 @@ class NativeVideoViewerPage extends HookConsumerWidget { // 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 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'; @@ -117,8 +117,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { } try { - aspectRatio.value = - await ref.read(assetServiceProvider).getAspectRatio(asset); + aspectRatio.value = await ref.read(assetServiceProvider).getAspectRatio(asset); } catch (error) { log.severe( 'Error getting aspect ratio for asset ${asset.fileName}: $error', @@ -133,8 +132,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { } final videoPlayback = ref.read(videoPlaybackValueProvider); - if ((isBuffering.value || - videoPlayback.state == VideoPlaybackState.initializing) && + if ((isBuffering.value || videoPlayback.state == VideoPlaybackState.initializing) && videoPlayback.state != VideoPlaybackState.buffering) { ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback.copyWith(state: VideoPlaybackState.buffering); @@ -193,10 +191,11 @@ class NativeVideoViewerPage extends HookConsumerWidget { return; } - final videoPlayback = - VideoPlaybackValue.fromNativeController(videoController); + final videoPlayback = VideoPlaybackValue.fromNativeController(videoController); ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; + isVideoReady.value = true; + try { await videoController.play(); await videoController.setVolume(0.9); @@ -211,8 +210,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { return; } - final videoPlayback = - VideoPlaybackValue.fromNativeController(videoController); + final videoPlayback = VideoPlaybackValue.fromNativeController(videoController); if (videoPlayback.state == VideoPlaybackState.playing) { // Sync with the controls playing WakelockPlus.enable(); @@ -221,8 +219,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { WakelockPlus.disable(); } - ref.read(videoPlaybackValueProvider.notifier).status = - videoPlayback.state; + ref.read(videoPlaybackValueProvider.notifier).status = videoPlayback.state; } void onPlaybackPositionChanged() { @@ -241,8 +238,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { return; } - ref.read(videoPlaybackValueProvider.notifier).position = - Duration(seconds: playbackInfo.position); + ref.read(videoPlaybackValueProvider.notifier).position = Duration(seconds: playbackInfo.position); // Check if the video is buffering if (playbackInfo.status == PlaybackStatus.playing) { @@ -261,18 +257,14 @@ class NativeVideoViewerPage extends HookConsumerWidget { } if (videoController.playbackInfo?.status == PlaybackStatus.stopped && - !ref - .read(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.loopVideo)) { + !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.onPlaybackPositionChanged.removeListener(onPlaybackPositionChanged); + controller.onPlaybackStatusChanged.removeListener(onPlaybackStatusChanged); controller.onPlaybackReady.removeListener(onPlaybackReady); controller.onPlaybackEnded.removeListener(onPlaybackEnded); } @@ -297,9 +289,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { nc.loadVideoSource(source).catchError((error) { log.severe('Error loading video source: $error'); }); - final loopVideo = ref - .read(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.loopVideo); + final loopVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo); nc.setLoop(loopVideo); controller.value = nc; @@ -393,7 +383,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { children: [ // This remains under the video to avoid flickering // For motion videos, this is the image portion of the asset - Center(key: ValueKey(asset.id), child: image), + if (!isVideoReady.value || asset.isMotionPhoto) Center(key: ValueKey(asset.id), child: image), if (aspectRatio.value != null && !isCasting) Visibility.maintain( key: ValueKey(asset), diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index 05c7606970..d18a7a1133 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -1,19 +1,28 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_hooks/flutter_hooks.dart' hide Store; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package: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/beta_sync_settings/beta_sync_settings.dart'; +import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; import 'package:immich_mobile/widgets/settings/language_settings.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; import 'package:immich_mobile/widgets/settings/notification_setting.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart'; -import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/settings/settings_card.dart'; enum SettingSection { + beta( + 'beta_sync', + Icons.sync_outlined, + "beta_sync_subtitle", + ), advanced( 'advanced', Icons.build_outlined, @@ -25,7 +34,7 @@ enum SettingSection { "asset_viewer_settings_subtitle", ), backup( - 'backup_controller_page_backup', + 'backup', Icons.cloud_upload_outlined, "backup_setting_subtitle", ), @@ -60,6 +69,7 @@ enum SettingSection { final IconData icon; Widget get widget => switch (this) { + SettingSection.beta => const _BetaLandscapeToggle(), SettingSection.advanced => const AdvancedSettings(), SettingSection.assetViewer => const AssetViewerSettings(), SettingSection.backup => const BackupSettings(), @@ -85,72 +95,51 @@ class SettingsPage extends StatelessWidget { centerTitle: false, title: const Text('settings').tr(), ), - body: context.isMobile ? _MobileLayout() : _TabletLayout(), + body: context.isMobile ? const _MobileLayout() : const _TabletLayout(), ); } } class _MobileLayout extends StatelessWidget { + const _MobileLayout(); @override Widget build(BuildContext context) { + final List settings = SettingSection.values + .expand( + (setting) => setting == SettingSection.beta + ? [ + const BetaTimelineListTile(), + if (Store.isBetaTimelineEnabled) + SettingsCard( + icon: Icons.sync_outlined, + title: 'beta_sync'.tr(), + subtitle: 'beta_sync_subtitle'.tr(), + settingRoute: const BetaSyncSettingsRoute(), + ), + ] + : [ + SettingsCard( + title: setting.title.tr(), + subtitle: setting.subtitle.tr(), + icon: setting.icon, + settingRoute: SettingsSubRoute(section: setting), + ), + ], + ) + .toList(); return ListView( physics: const ClampingScrollPhysics(), - padding: const EdgeInsets.symmetric(vertical: 10.0), - children: SettingSection.values - .map( - (setting) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - ), - child: Card( - elevation: 0, - clipBehavior: Clip.antiAlias, - color: context.colorScheme.surfaceContainer, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16)), - ), - margin: const EdgeInsets.symmetric(vertical: 4.0), - child: ListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16.0, - ), - leading: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(16)), - color: context.isDarkTheme - ? Colors.black26 - : Colors.white.withAlpha(100), - ), - padding: const EdgeInsets.all(16.0), - child: Icon(setting.icon, color: context.primaryColor), - ), - title: Text( - setting.title, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, - ), - ).tr(), - subtitle: Text( - setting.subtitle, - style: context.textTheme.labelLarge, - ).tr(), - onTap: () => - context.pushRoute(SettingsSubRoute(section: setting)), - ), - ), - ), - ) - .toList(), + padding: const EdgeInsets.only(top: 10.0, bottom: 56), + children: [...settings], ); } } class _TabletLayout extends HookWidget { + const _TabletLayout(); @override Widget build(BuildContext context) { - final selectedSection = - useState(SettingSection.values.first); + final selectedSection = useState(SettingSection.values.first); return Row( mainAxisAlignment: MainAxisAlignment.start, @@ -158,20 +147,20 @@ class _TabletLayout extends HookWidget { Expanded( flex: 2, child: CustomScrollView( - slivers: SettingSection.values - .map( - (s) => SliverToBoxAdapter( - child: ListTile( - title: Text(s.title).tr(), - leading: Icon(s.icon), - selected: s.index == selectedSection.value.index, - selectedColor: context.primaryColor, - selectedTileColor: context.themeData.highlightColor, - onTap: () => selectedSection.value = s, - ), + slivers: [ + ...SettingSection.values.map( + (s) => SliverToBoxAdapter( + child: ListTile( + title: Text(s.title).tr(), + leading: Icon(s.icon), + selected: s.index == selectedSection.value.index, + selectedColor: context.primaryColor, + selectedTileColor: context.themeData.highlightColor, + onTap: () => selectedSection.value = s, ), - ) - .toList(), + ), + ), + ], ), ), const VerticalDivider(width: 1), @@ -184,6 +173,21 @@ class _TabletLayout extends HookWidget { } } +class _BetaLandscapeToggle extends HookWidget { + const _BetaLandscapeToggle(); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(height: 100, child: BetaTimelineListTile()), + if (Store.isBetaTimelineEnabled) const Expanded(child: BetaSyncSettings()), + ], + ); + } +} + @RoutePage() class SettingsSubPage extends StatelessWidget { const SettingsSubPage(this.section, {super.key}); diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index b640aaa3ed..47cd64f7f9 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -42,42 +42,39 @@ class SplashScreenPageState extends ConsumerState { final endpoint = Store.tryGet(StoreKey.serverEndpoint); final accessToken = Store.tryGet(StoreKey.accessToken); - bool isAuthSuccess = false; - if (accessToken != null && serverUrl != null && endpoint != null) { - try { - isAuthSuccess = await ref.read(authProvider.notifier).saveAuthInfo( - accessToken: accessToken, - ); - } catch (error, stackTrace) { - log.severe( - 'Cannot set success login info', - error, - stackTrace, - ); - } + ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then( + (a) => { + log.info('Successfully updated auth info with access token: $accessToken'), + }, + onError: (exception) => { + log.severe( + 'Failed to update auth info with access token: $accessToken', + ), + ref.read(authProvider.notifier).logout(), + context.replaceRoute(const LoginRoute()), + }, + ); } else { - isAuthSuccess = false; log.severe( - 'Missing authentication, server, or endpoint info from the local store', - ); - } - - if (!isAuthSuccess) { - log.severe( - 'Unable to login using offline or online methods - Logging out completely', + 'Missing crucial offline login info - Logging out completely', ); ref.read(authProvider.notifier).logout(); context.replaceRoute(const LoginRoute()); return; } - if (context.router.current.name != ShareIntentRoute.name) { - context.replaceRoute(const TabControllerRoute()); + if (context.router.current.name == SplashScreenRoute.name) { + context.replaceRoute( + Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute(), + ); } - final hasPermission = - await ref.read(galleryPermissionNotifier.notifier).hasPermission; + if (Store.isBetaTimelineEnabled) { + return; + } + + final hasPermission = await ref.read(galleryPermissionNotifier.notifier).hasPermission; if (hasPermission) { // Resume backup (if enable) then navigate ref.watch(backupProvider.notifier).resumeBackup(); diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart index 0188d953dc..676b1db11b 100644 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ b/mobile/lib/pages/common/tab_controller.page.dart @@ -4,13 +4,13 @@ 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/routing/router.dart'; -import 'package:immich_mobile/providers/asset.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'; @RoutePage() class TabControllerPage extends HookConsumerWidget { @@ -20,8 +20,7 @@ class TabControllerPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isRefreshingAssets = ref.watch(assetProvider); final isRefreshingRemoteAlbums = ref.watch(isRefreshingRemoteAlbumProvider); - final isScreenLandscape = - MediaQuery.orientationOf(context) == Orientation.landscape; + final isScreenLandscape = MediaQuery.orientationOf(context) == Orientation.landscape; Widget buildIcon({required Widget icon, required bool isProcessing}) { if (!isProcessing) return icon; @@ -118,8 +117,7 @@ class TabControllerPage extends HookConsumerWidget { Widget bottomNavigationBar(TabsRouter tabsRouter) { return NavigationBar( selectedIndex: tabsRouter.activeIndex, - onDestinationSelected: (index) => - onNavigationSelected(tabsRouter, index), + onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index), destinations: navigationDestinations, ); } @@ -135,8 +133,7 @@ class TabControllerPage extends HookConsumerWidget { ), ) .toList(), - onDestinationSelected: (index) => - onNavigationSelected(tabsRouter, index), + onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index), selectedIndex: tabsRouter.activeIndex, labelType: NavigationRailLabelType.all, groupAlignment: 0.0, @@ -158,14 +155,9 @@ class TabControllerPage extends HookConsumerWidget { ), builder: (context, child) { final tabsRouter = AutoTabsRouter.of(context); - final heroedChild = HeroControllerScope( - controller: HeroController(), - child: child, - ); return PopScope( canPop: tabsRouter.activeIndex == 0, - onPopInvokedWithResult: (didPop, _) => - !didPop ? tabsRouter.setActiveIndex(0) : null, + onPopInvokedWithResult: (didPop, _) => !didPop ? tabsRouter.setActiveIndex(0) : null, child: Scaffold( resizeToAvoidBottomInset: false, body: isScreenLandscape @@ -173,13 +165,11 @@ class TabControllerPage extends HookConsumerWidget { children: [ navigationRail(tabsRouter), const VerticalDivider(), - Expanded(child: heroedChild), + Expanded(child: child), ], ) - : heroedChild, - bottomNavigationBar: multiselectEnabled || isScreenLandscape - ? null - : bottomNavigationBar(tabsRouter), + : child, + bottomNavigationBar: multiselectEnabled || isScreenLandscape ? null : bottomNavigationBar(tabsRouter), ), ); }, diff --git a/mobile/lib/pages/common/tab_shell.page.dart b/mobile/lib/pages/common/tab_shell.page.dart new file mode 100644 index 0000000000..5129f6b159 --- /dev/null +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -0,0 +1,200 @@ +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/app_settings.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; +import 'package:immich_mobile/providers/tab.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/providers/websocket.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/migration.dart'; + +@RoutePage() +class TabShellPage extends ConsumerStatefulWidget { + const TabShellPage({super.key}); + + @override + ConsumerState createState() => _TabShellPageState(); +} + +class _TabShellPageState extends ConsumerState { + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + ref.read(websocketProvider.notifier).connect(); + + final isEnableBackup = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); + + await runNewSync(ref, full: true).then((_) async { + if (isEnableBackup) { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + await ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); + } + }); + }); + } + + @override + Widget build(BuildContext context) { + final isScreenLandscape = context.orientation == Orientation.landscape; + + final navigationDestinations = [ + NavigationDestination( + label: 'photos'.tr(), + icon: const Icon( + Icons.photo_library_outlined, + ), + selectedIcon: 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: Icon( + Icons.photo_album_rounded, + color: context.primaryColor, + ), + ), + NavigationDestination( + label: 'library'.tr(), + icon: const Icon( + Icons.space_dashboard_outlined, + ), + selectedIcon: Icon( + Icons.space_dashboard_rounded, + color: context.primaryColor, + ), + ), + ]; + + 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, ref), + selectedIndex: tabsRouter.activeIndex, + labelType: NavigationRailLabelType.all, + groupAlignment: 0.0, + ); + } + + return AutoTabsRouter( + routes: [ + const MainTimelineRoute(), + DriftSearchRoute(), + const DriftAlbumsRoute(), + const DriftLibraryRoute(), + ], + 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: _BottomNavigationBar( + tabsRouter: tabsRouter, + destinations: navigationDestinations, + ), + ), + ); + }, + ); + } +} + +void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) { + // 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(); + } + + // Album page + if (index == 2) { + ref.read(remoteAlbumProvider.notifier).refresh(); + } + + ref.read(hapticFeedbackProvider.notifier).selectionClick(); + router.setActiveIndex(index); + ref.read(tabProvider.notifier).state = TabEnum.values[index]; +} + +class _BottomNavigationBar extends ConsumerWidget { + const _BottomNavigationBar({ + required this.tabsRouter, + required this.destinations, + }); + + final List destinations; + final TabsRouter tabsRouter; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isScreenLandscape = context.orientation == Orientation.landscape; + final isMultiselectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled)); + + if (isScreenLandscape || isMultiselectEnabled) { + return const SizedBox.shrink(); + } + + return NavigationBar( + selectedIndex: tabsRouter.activeIndex, + onDestinationSelected: (index) => _onNavigationSelected(tabsRouter, index, ref), + destinations: destinations, + ); + } +} diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart index f7f459c770..97ef069e7b 100644 --- a/mobile/lib/pages/editing/crop.page.dart +++ b/mobile/lib/pages/editing/crop.page.dart @@ -187,9 +187,7 @@ class _AspectRatioButton extends StatelessWidget { '7:5' => Icons.crop_7_5_rounded, _ => Icons.crop_free_rounded, }, - color: aspectRatio.value == ratio - ? context.primaryColor - : context.themeData.iconTheme.color, + color: aspectRatio.value == ratio ? context.primaryColor : context.themeData.iconTheme.color, ), onPressed: () { cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart index 39524df024..70940e9552 100644 --- a/mobile/lib/pages/editing/edit.page.dart +++ b/mobile/lib/pages/editing/edit.page.dart @@ -40,9 +40,7 @@ class EditImagePage extends ConsumerWidget { image.image.resolve(const ImageConfiguration()).addListener( ImageStreamListener( (ImageInfo info, bool _) { - info.image - .toByteData(format: ImageByteFormat.png) - .then((byteData) { + info.image.toByteData(format: ImageByteFormat.png).then((byteData) { if (byteData != null) { completer.complete(byteData.buffer.asUint8List()); } else { @@ -50,8 +48,7 @@ class EditImagePage extends ConsumerWidget { } }); }, - onError: (exception, stackTrace) => - completer.completeError(exception), + onError: (exception, stackTrace) => completer.completeError(exception), ), ); return completer.future; @@ -103,9 +100,7 @@ class EditImagePage extends ConsumerWidget { ), actions: [ TextButton( - onPressed: isEdited - ? () => _saveEditedImage(context, asset, image, ref) - : null, + onPressed: isEdited ? () => _saveEditedImage(context, asset, image, ref) : null, child: Text( "save_to_gallery".tr(), style: TextStyle( @@ -124,7 +119,9 @@ class EditImagePage extends ConsumerWidget { ), child: Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(7), + borderRadius: const BorderRadius.all( + Radius.circular(7), + ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.2), @@ -135,7 +132,9 @@ class EditImagePage extends ConsumerWidget { ], ), child: ClipRRect( - borderRadius: BorderRadius.circular(7), + borderRadius: const BorderRadius.all( + Radius.circular(7), + ), child: Image( image: image.image, fit: BoxFit.contain, @@ -149,7 +148,9 @@ class EditImagePage extends ConsumerWidget { margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), decoration: BoxDecoration( color: context.scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(30), + borderRadius: const BorderRadius.all( + Radius.circular(30), + ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/mobile/lib/pages/editing/filter.page.dart b/mobile/lib/pages/editing/filter.page.dart index f8b1f180df..3dc8d28a94 100644 --- a/mobile/lib/pages/editing/filter.page.dart +++ b/mobile/lib/pages/editing/filter.page.dart @@ -34,18 +34,14 @@ class FilterImagePage extends HookWidget { ColorFilter filter, ) { final completer = Completer(); - final size = - Size(inputImage.width.toDouble(), inputImage.height.toDouble()); + 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) { + recorder.endRecording().toImage(size.width.round(), size.height.round()).then((image) { completer.complete(image); }); @@ -67,8 +63,7 @@ class FilterImagePage extends HookWidget { final uiImage = await completer.future; final filteredUiImage = await createFilteredImage(uiImage, filter); - final byteData = - await filteredUiImage.toByteData(format: ui.ImageByteFormat.png); + final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png); final pngBytes = byteData!.buffer.asUint8List(); return Image.memory(pngBytes, fit: BoxFit.contain); @@ -87,8 +82,7 @@ class FilterImagePage extends HookWidget { size: 24, ), onPressed: () async { - final filteredImage = - await applyFilterAndConvert(colorFilter.value); + final filteredImage = await applyFilterAndConvert(colorFilter.value); context.pushRoute( EditImageRoute( asset: asset, @@ -162,13 +156,15 @@ class _FilterButton extends StatelessWidget { width: 80, height: 80, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: isSelected - ? Border.all(color: context.primaryColor, width: 3) - : null, + borderRadius: const BorderRadius.all( + Radius.circular(10), + ), + border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null, ), child: ClipRRect( - borderRadius: BorderRadius.circular(10), + borderRadius: const BorderRadius.all( + Radius.circular(10), + ), child: ColorFiltered( colorFilter: filter, child: FittedBox( diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index 6ac7d60f9b..d089aace6e 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -21,9 +21,7 @@ RecursiveFolder? _findFolderInStructure( RecursiveFolder targetFolder, ) { for (final folder in rootFolder.subfolders) { - if (targetFolder.path == '/' && - folder.path.isEmpty && - folder.name == targetFolder.name) { + if (targetFolder.path == '/' && folder.path.isEmpty && folder.name == targetFolder.name) { return folder; } @@ -54,9 +52,7 @@ class FolderPage extends HookConsumerWidget { useEffect( () { if (folder == null) { - ref - .read(folderStructureProvider.notifier) - .fetchFolders(sortOrder.value); + ref.read(folderStructureProvider.notifier).fetchFolders(sortOrder.value); } return null; }, @@ -67,8 +63,7 @@ class FolderPage extends HookConsumerWidget { useEffect( () { if (folder != null && folderState.hasValue) { - final updatedFolder = - _findFolderInStructure(folderState.value!, folder!); + final updatedFolder = _findFolderInStructure(folderState.value!, folder!); if (updatedFolder != null) { currentFolder.value = updatedFolder; } @@ -79,8 +74,7 @@ class FolderPage extends HookConsumerWidget { ); void onToggleSortOrder() { - final newOrder = - sortOrder.value == SortOrder.asc ? SortOrder.desc : SortOrder.asc; + final newOrder = sortOrder.value == SortOrder.asc ? SortOrder.desc : SortOrder.asc; ref.read(folderStructureProvider.notifier).fetchFolders(newOrder); @@ -151,9 +145,7 @@ class FolderContent extends HookConsumerWidget { useEffect( () { if (folder == null) return; - ref - .read(folderRenderListProvider(folder!).notifier) - .fetchAssets(sortOrder); + ref.read(folderRenderListProvider(folder!).notifier).fetchAssets(sortOrder); return null; }, [folder], @@ -211,13 +203,10 @@ class FolderContent extends HookConsumerWidget { ), ) : null, - onTap: () => - context.pushRoute(FolderRoute(folder: subfolder)), + onTap: () => context.pushRoute(FolderRoute(folder: subfolder)), ), ), - if (!list.isEmpty && - list.allAssets != null && - list.allAssets!.isNotEmpty) + if (!list.isEmpty && list.allAssets != null && list.allAssets!.isNotEmpty) ...list.allAssets!.map( (asset) => LargeLeadingTile( onTap: () { diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 37b5e28797..f51817d067 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -25,8 +25,7 @@ class LibraryPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { context.locale; - final trashEnabled = - ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); + final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); return Scaffold( appBar: const ImmichAppBar(), @@ -105,7 +104,9 @@ class QuickAccessButtons extends ConsumerWidget { color: context.colorScheme.onSurface.withAlpha(10), width: 1, ), - borderRadius: BorderRadius.circular(20), + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), gradient: LinearGradient( colors: [ context.colorScheme.primary.withAlpha(10), @@ -240,7 +241,9 @@ class PeopleCollectionCard extends ConsumerWidget { height: size, width: size, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), gradient: LinearGradient( colors: [ context.colorScheme.primary.withAlpha(30), @@ -384,8 +387,7 @@ class PlacesCollectionCard extends StatelessWidget { child: DecoratedBox( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(20)), - color: - context.colorScheme.secondaryContainer.withAlpha(100), + color: context.colorScheme.secondaryContainer.withAlpha(100), ), child: IgnorePointer( child: MapThumbnail( @@ -395,9 +397,7 @@ class PlacesCollectionCard extends StatelessWidget { -157.91959, ), showAttribution: false, - themeMode: context.isDarkTheme - ? ThemeMode.dark - : ThemeMode.light, + themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, ), ), ), diff --git a/mobile/lib/pages/library/local_albums.page.dart b/mobile/lib/pages/library/local_albums.page.dart index 9eceaca205..5c6091c76d 100644 --- a/mobile/lib/pages/library/local_albums.page.dart +++ b/mobile/lib/pages/library/local_albums.page.dart @@ -54,8 +54,7 @@ class LocalAlbumsPage extends HookConsumerWidget { color: context.colorScheme.onSurfaceSecondary, ), ), - onTap: () => context - .pushRoute(AlbumViewerRoute(albumId: albums[index].id)), + onTap: () => context.pushRoute(AlbumViewerRoute(albumId: albums[index].id)), ), ); }, diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index cca0e3b7ac..abe2247927 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -1,13 +1,14 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_hooks/flutter_hooks.dart' show useState; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/local_auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/forms/pin_registration_form.dart'; import 'package:immich_mobile/widgets/forms/pin_verification_form.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; @RoutePage() class PinAuthPage extends HookConsumerWidget { @@ -19,13 +20,13 @@ 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, - ); + final isRegistered = await ref.read(localAuthProvider.notifier).registerBiometric( + context, + pinCode, + ); if (isRegistered) { context.showSnackBar( @@ -39,7 +40,11 @@ class PinAuthPage extends HookConsumerWidget { ), ); - context.replaceRoute(const LockedRoute()); + if (isBetaTimeline) { + context.replaceRoute(const DriftLockedFolderRoute()); + } else { + context.replaceRoute(const LockedRoute()); + } } } @@ -93,8 +98,13 @@ class PinAuthPage extends HookConsumerWidget { Center( child: PinVerificationForm( autoFocus: true, - onSuccess: (_) => - context.replaceRoute(const LockedRoute()), + onSuccess: (_) { + if (isBetaTimeline) { + context.replaceRoute(const DriftLockedFolderRoute()); + } else { + context.replaceRoute(const LockedRoute()); + } + }, ), ), const SizedBox(height: 24), diff --git a/mobile/lib/pages/library/partner/drift_partner.page.dart b/mobile/lib/pages/library/partner/drift_partner.page.dart new file mode 100644 index 0000000000..d65f2bc094 --- /dev/null +++ b/mobile/lib/pages/library/partner/drift_partner.page.dart @@ -0,0 +1,159 @@ +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/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/partner_user_avatar.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/partner.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; +import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +@RoutePage() +class DriftPartnerPage extends HookConsumerWidget { + const DriftPartnerPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final potentialPartnersAsync = ref.watch(driftAvailablePartnerProvider); + + addNewUsersHandler() async { + final potentialPartners = potentialPartnersAsync.value; + if (potentialPartners == null || potentialPartners.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 (PartnerUserDto partner in potentialPartners) + SimpleDialogOption( + onPressed: () => context.pop(partner), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: PartnerUserAvatar(partner: partner), + ), + Text(partner.name), + ], + ), + ), + ], + ); + }, + ); + if (selectedUser != null) { + await ref.read(partnerUsersProvider.notifier).addPartner(selectedUser); + } + } + + onDeleteUser(PartnerUserDto partner) { + return showDialog( + context: context, + builder: (BuildContext context) { + return ConfirmDialog( + title: "stop_photo_sharing", + content: "partner_page_stop_sharing_content".tr(namedArgs: {'partner': partner.name}), + onOk: () => ref.read(partnerUsersProvider.notifier).removePartner(partner), + ); + }, + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text("partners").t(context: context), + elevation: 0, + centerTitle: false, + actions: [ + IconButton( + onPressed: potentialPartnersAsync.whenOrNull( + data: (data) => addNewUsersHandler, + ), + icon: const Icon(Icons.person_add), + tooltip: "add_partner".tr(), + ), + ], + ), + body: _SharedToPartnerList( + onAddPartner: addNewUsersHandler, + onDeletePartner: onDeleteUser, + ), + ); + } +} + +class _SharedToPartnerList extends ConsumerWidget { + final VoidCallback onAddPartner; + final Function(PartnerUserDto partner) onDeletePartner; + + const _SharedToPartnerList({ + required this.onAddPartner, + required this.onDeletePartner, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final partnerAsync = ref.watch(driftSharedByPartnerProvider); + + return partnerAsync.when( + data: (partners) { + if (partners.isEmpty) { + return 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: onAddPartner, + icon: const Icon(Icons.person_add), + label: const Text("add_partner").tr(), + ), + ), + ], + ), + ); + } + + return ListView.builder( + itemCount: partners.length, + itemBuilder: (context, index) { + final partner = partners[index]; + return ListTile( + leading: PartnerUserAvatar(partner: partner), + title: Text(partner.name), + subtitle: Text(partner.email), + trailing: IconButton( + icon: const Icon(Icons.person_remove), + onPressed: () => onDeletePartner(partner), + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center( + child: Text("Error loading partners: $error"), + ), + ); + } +} diff --git a/mobile/lib/pages/library/partner/partner.page.dart b/mobile/lib/pages/library/partner/partner.page.dart index 91b661e7ce..fb0dfe2ec3 100644 --- a/mobile/lib/pages/library/partner/partner.page.dart +++ b/mobile/lib/pages/library/partner/partner.page.dart @@ -53,8 +53,7 @@ class PartnerPage extends HookConsumerWidget { }, ); if (selectedUser != null) { - final ok = - await ref.read(partnerServiceProvider).addPartner(selectedUser); + final ok = await ref.read(partnerServiceProvider).addPartner(selectedUser); if (ok) { ref.invalidate(partnerSharedByProvider); } else { @@ -73,8 +72,7 @@ class PartnerPage extends HookConsumerWidget { builder: (BuildContext context) { return ConfirmDialog( title: "stop_photo_sharing", - content: "partner_page_stop_sharing_content" - .tr(namedArgs: {'partner': u.name}), + content: "partner_page_stop_sharing_content".tr(namedArgs: {'partner': u.name}), onOk: () => ref.read(partnerServiceProvider).removePartner(u), ); }, @@ -149,8 +147,7 @@ class PartnerPage extends HookConsumerWidget { centerTitle: false, actions: [ IconButton( - onPressed: - availableUsers.whenOrNull(data: (data) => addNewUsersHandler), + onPressed: availableUsers.whenOrNull(data: (data) => addNewUsersHandler), icon: const Icon(Icons.person_add), tooltip: "add_partner".tr(), ), diff --git a/mobile/lib/pages/library/partner/partner_detail.page.dart b/mobile/lib/pages/library/partner/partner_detail.page.dart index 618d31affa..78af3f0939 100644 --- a/mobile/lib/pages/library/partner/partner_detail.page.dart +++ b/mobile/lib/pages/library/partner/partner_detail.page.dart @@ -38,9 +38,8 @@ class PartnerDetailPage extends HookConsumerWidget { if (toggleInProcess) return; toggleInProcess = true; try { - final ok = await ref - .read(partnerSharedWithProvider.notifier) - .updatePartner(partner, inTimeline: !inTimeline.value); + 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"; @@ -80,7 +79,9 @@ class PartnerDetailPage extends HookConsumerWidget { color: context.colorScheme.onSurface.withAlpha(10), width: 1, ), - borderRadius: BorderRadius.circular(20), + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), gradient: LinearGradient( colors: [ context.colorScheme.primary.withAlpha(10), diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart index b98e46aabe..837553ac40 100644 --- a/mobile/lib/pages/library/people/people_collection.page.dart +++ b/mobile/lib/pages/library/people/people_collection.page.dart @@ -66,9 +66,7 @@ class PeopleCollectionPage extends HookConsumerWidget { data: (people) { if (search.value != null) { people = people.where((person) { - return person.name - .toLowerCase() - .contains(search.value!.toLowerCase()); + return person.name.toLowerCase().contains(search.value!.toLowerCase()); }).toList(); } return GridView.builder( @@ -107,8 +105,7 @@ class PeopleCollectionPage extends HookConsumerWidget { ), const SizedBox(height: 12), GestureDetector( - onTap: () => - showNameEditModel(person.id, person.name), + onTap: () => showNameEditModel(person.id, person.name), child: person.name.isEmpty ? Text( 'add_a_name'.tr(), @@ -124,8 +121,7 @@ class PeopleCollectionPage extends HookConsumerWidget { child: Text( person.name, overflow: TextOverflow.ellipsis, - style: - context.textTheme.titleSmall?.copyWith( + style: context.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, ), ), diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart index 5f2dea0dec..98bf372a96 100644 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ b/mobile/lib/pages/library/places/places_collection.page.dart @@ -59,8 +59,7 @@ class PlacesCollectionPage extends HookConsumerWidget { height: 200, width: context.width, child: MapThumbnail( - onTap: (_, __) => context - .pushRoute(MapRoute(initialLocation: currentLocation)), + onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)), zoom: 8, centre: currentLocation ?? const LatLng( @@ -68,8 +67,7 @@ class PlacesCollectionPage extends HookConsumerWidget { -157.91959, ), showAttribution: false, - themeMode: - context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, + themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, ), ), ), @@ -77,9 +75,7 @@ class PlacesCollectionPage extends HookConsumerWidget { data: (places) { if (search.value != null) { places = places.where((place) { - return place.label - .toLowerCase() - .contains(search.value!.toLowerCase()); + return place.label.toLowerCase().contains(search.value!.toLowerCase()); }).toList(); } return ListView.builder( @@ -110,8 +106,7 @@ class PlaceTile extends StatelessWidget { @override Widget build(BuildContext context) { - final thumbnailUrl = - '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail'; + final thumbnailUrl = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail'; void navigateToPlace() { context.pushRoute( @@ -143,15 +138,16 @@ class PlaceTile extends StatelessWidget { ), ), leading: ClipRRect( - borderRadius: BorderRadius.circular(20), + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), child: CachedNetworkImage( width: 80, height: 80, fit: BoxFit.cover, imageUrl: thumbnailUrl, httpHeaders: ApiService.getRequestHeaders(), - errorWidget: (context, url, error) => - const Icon(Icons.image_not_supported_outlined), + errorWidget: (context, url, error) => const Icon(Icons.image_not_supported_outlined), ), ), ); diff --git a/mobile/lib/pages/library/shared_link/shared_link.page.dart b/mobile/lib/pages/library/shared_link/shared_link.page.dart index 8873e9b443..94af8f913b 100644 --- a/mobile/lib/pages/library/shared_link/shared_link.page.dart +++ b/mobile/lib/pages/library/shared_link/shared_link.page.dart @@ -58,8 +58,7 @@ class SharedLinkPage extends HookConsumerWidget { child: Icon( Icons.link_off, size: 100, - color: - context.themeData.iconTheme.color?.withValues(alpha: 0.5), + color: context.themeData.iconTheme.color?.withValues(alpha: 0.5), ), ), ), @@ -86,8 +85,7 @@ class SharedLinkPage extends HookConsumerWidget { if (constraints.maxWidth > 600) { // Two column return GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisExtent: 180, ), @@ -121,8 +119,7 @@ class SharedLinkPage extends HookConsumerWidget { body: SafeArea( child: sharedLinks.widgetWhen( onError: (error, stackTrace) => buildNoShares(), - onData: (links) => - links.isNotEmpty ? buildSharesList(links) : buildNoShares(), + onData: (links) => links.isNotEmpty ? buildSharesList(links) : buildNoShares(), ), ), ); diff --git a/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart index 6c18841089..c6db85a0fa 100644 --- a/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart +++ b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart @@ -31,11 +31,9 @@ class SharedLinkEditPage extends HookConsumerWidget { const padding = 20.0; final themeData = context.themeData; final colorScheme = context.colorScheme; - final descriptionController = - useTextEditingController(text: existingLink?.description ?? ""); + final descriptionController = useTextEditingController(text: existingLink?.description ?? ""); final descriptionFocusNode = useFocusNode(); - final passwordController = - useTextEditingController(text: existingLink?.password ?? ""); + final passwordController = useTextEditingController(text: existingLink?.password ?? ""); final showMetadata = useState(existingLink?.showMetadata ?? true); final allowDownload = useState(existingLink?.allowDownload ?? true); final allowUpload = useState(existingLink?.allowUpload ?? false); @@ -155,15 +153,12 @@ class SharedLinkEditPage extends HookConsumerWidget { Widget buildShowMetaButton() { return SwitchListTile.adaptive( value: showMetadata.value, - onChanged: newShareLink.value.isEmpty - ? (value) => showMetadata.value = value - : null, + onChanged: newShareLink.value.isEmpty ? (value) => showMetadata.value = value : null, activeColor: colorScheme.primary, dense: true, title: Text( "show_metadata", - style: themeData.textTheme.labelLarge - ?.copyWith(fontWeight: FontWeight.bold), + style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold), ).tr(), ); } @@ -171,15 +166,12 @@ class SharedLinkEditPage extends HookConsumerWidget { Widget buildAllowDownloadButton() { return SwitchListTile.adaptive( value: allowDownload.value, - onChanged: newShareLink.value.isEmpty - ? (value) => allowDownload.value = value - : null, + onChanged: newShareLink.value.isEmpty ? (value) => allowDownload.value = value : null, activeColor: colorScheme.primary, dense: true, title: Text( "allow_public_user_to_download", - style: themeData.textTheme.labelLarge - ?.copyWith(fontWeight: FontWeight.bold), + style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold), ).tr(), ); } @@ -187,15 +179,12 @@ class SharedLinkEditPage extends HookConsumerWidget { Widget buildAllowUploadButton() { return SwitchListTile.adaptive( value: allowUpload.value, - onChanged: newShareLink.value.isEmpty - ? (value) => allowUpload.value = value - : null, + onChanged: newShareLink.value.isEmpty ? (value) => allowUpload.value = value : null, activeColor: colorScheme.primary, dense: true, title: Text( "allow_public_user_to_upload", - style: themeData.textTheme.labelLarge - ?.copyWith(fontWeight: FontWeight.bold), + style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold), ).tr(), ); } @@ -203,15 +192,12 @@ class SharedLinkEditPage extends HookConsumerWidget { Widget buildEditExpiryButton() { return SwitchListTile.adaptive( value: editExpiry.value, - onChanged: newShareLink.value.isEmpty - ? (value) => editExpiry.value = value - : null, + onChanged: newShareLink.value.isEmpty ? (value) => editExpiry.value = value : null, activeColor: colorScheme.primary, dense: true, title: Text( "change_expiration_time", - style: themeData.textTheme.labelLarge - ?.copyWith(fontWeight: FontWeight.bold), + style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold), ).tr(), ); } @@ -229,8 +215,7 @@ class SharedLinkEditPage extends HookConsumerWidget { enableFilter: false, width: context.width - 40, initialSelection: expiryAfter.value, - enabled: newShareLink.value.isEmpty && - (existingLink == null || editExpiry.value), + enabled: newShareLink.value.isEmpty && (existingLink == null || editExpiry.value), onSelected: (value) { expiryAfter.value = value!; }, @@ -241,8 +226,7 @@ class SharedLinkEditPage extends HookConsumerWidget { ), DropdownMenuEntry( value: 30, - label: "shared_link_edit_expire_after_option_minutes" - .tr(namedArgs: {'count': "30"}), + label: "shared_link_edit_expire_after_option_minutes".tr(namedArgs: {'count': "30"}), ), DropdownMenuEntry( value: 60, @@ -250,8 +234,7 @@ class SharedLinkEditPage extends HookConsumerWidget { ), DropdownMenuEntry( value: 60 * 6, - label: "shared_link_edit_expire_after_option_hours" - .tr(namedArgs: {'count': "6"}), + label: "shared_link_edit_expire_after_option_hours".tr(namedArgs: {'count': "6"}), ), DropdownMenuEntry( value: 60 * 24, @@ -259,23 +242,19 @@ class SharedLinkEditPage extends HookConsumerWidget { ), DropdownMenuEntry( value: 60 * 24 * 7, - label: "shared_link_edit_expire_after_option_days" - .tr(namedArgs: {'count': "7"}), + label: "shared_link_edit_expire_after_option_days".tr(namedArgs: {'count': "7"}), ), DropdownMenuEntry( value: 60 * 24 * 30, - label: "shared_link_edit_expire_after_option_days" - .tr(namedArgs: {'count': "30"}), + label: "shared_link_edit_expire_after_option_days".tr(namedArgs: {'count': "30"}), ), DropdownMenuEntry( value: 60 * 24 * 30 * 3, - label: "shared_link_edit_expire_after_option_months" - .tr(namedArgs: {'count': "3"}), + label: "shared_link_edit_expire_after_option_months".tr(namedArgs: {'count': "3"}), ), DropdownMenuEntry( value: 60 * 24 * 30 * 12, - label: "shared_link_edit_expire_after_option_year" - .tr(namedArgs: {'count': "1"}), + label: "shared_link_edit_expire_after_option_year".tr(namedArgs: {'count': "1"}), ), ], ); @@ -346,27 +325,21 @@ class SharedLinkEditPage extends HookConsumerWidget { } Future handleNewLink() async { - final newLink = - await ref.read(sharedLinkServiceProvider).createSharedLink( - albumId: albumId, - assetIds: assetsList, - showMeta: showMetadata.value, - allowDownload: allowDownload.value, - allowUpload: allowUpload.value, - description: descriptionController.text.isEmpty - ? null - : descriptionController.text, - password: passwordController.text.isEmpty - ? null - : passwordController.text, - expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(), - ); + final newLink = await ref.read(sharedLinkServiceProvider).createSharedLink( + albumId: albumId, + assetIds: assetsList, + showMeta: showMetadata.value, + allowDownload: allowDownload.value, + allowUpload: allowUpload.value, + description: descriptionController.text.isEmpty ? null : descriptionController.text, + password: passwordController.text.isEmpty ? null : passwordController.text, + expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(), + ); ref.invalidate(sharedLinksStateProvider); final externalDomain = ref.read( serverInfoProvider.select((s) => s.serverConfig.externalDomain), ); - var serverUrl = - externalDomain.isNotEmpty ? externalDomain : getServerUrl(); + var serverUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl(); if (serverUrl != null && !serverUrl.endsWith('/')) { serverUrl += '/'; } @@ -472,8 +445,7 @@ class SharedLinkEditPage extends HookConsumerWidget { child: buildAllowDownloadButton(), ), Padding( - padding: - const EdgeInsets.only(left: padding, right: 20, bottom: 20), + padding: const EdgeInsets.only(left: padding, right: 20, bottom: 20), child: buildAllowUploadButton(), ), if (existingLink != null) @@ -502,12 +474,9 @@ class SharedLinkEditPage extends HookConsumerWidget { bottom: padding, ), child: ElevatedButton( - onPressed: - existingLink != null ? handleEditLink : handleNewLink, + onPressed: existingLink != null ? handleEditLink : handleNewLink, child: Text( - existingLink != null - ? "shared_link_edit_submit_button" - : "create_link", + existingLink != null ? "shared_link_edit_submit_button" : "create_link", style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, diff --git a/mobile/lib/pages/library/trash.page.dart b/mobile/lib/pages/library/trash.page.dart index c645719974..9ffe7ae704 100644 --- a/mobile/lib/pages/library/trash.page.dart +++ b/mobile/lib/pages/library/trash.page.dart @@ -24,8 +24,7 @@ class TrashPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final trashRenderList = ref.watch(trashTimelineProvider); - final trashDays = - ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays)); + final trashDays = ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays)); final selectionEnabledHook = useState(false); final selection = useState({}); final processing = useProcessingOverlay(); @@ -68,16 +67,13 @@ class TrashPage extends HookConsumerWidget { processing.value = true; try { if (selection.value.isNotEmpty) { - final isRemoved = await ref - .read(assetProvider.notifier) - .deleteAssets(selection.value, force: true); + 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}"}), + msg: 'assets_deleted_permanently'.tr(namedArgs: {'count': "${selection.value.length}"}), gravity: ToastGravity.BOTTOM, ); } @@ -110,15 +106,12 @@ class TrashPage extends HookConsumerWidget { processing.value = true; try { if (selection.value.isNotEmpty) { - final result = await ref - .read(trashProvider.notifier) - .restoreAssets(selection.value); + 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}"}), + msg: 'assets_restored_successfully'.tr(namedArgs: {'count': "${selection.value.length}"}), gravity: ToastGravity.BOTTOM, ); } @@ -131,9 +124,7 @@ class TrashPage extends HookConsumerWidget { String getAppBarTitle(String count) { if (selectionEnabledHook.value) { - return selection.value.isNotEmpty - ? "${selection.value.length}" - : "trash_page_select_assets_btn".tr(); + return selection.value.isNotEmpty ? "${selection.value.length}" : "trash_page_select_assets_btn".tr(); } return 'trash_page_title'.tr(namedArgs: {'count': count}); } @@ -147,9 +138,8 @@ class TrashPage extends HookConsumerWidget { selectionEnabledHook.value = false; selection.value = {}; }, - icon: !selectionEnabledHook.value - ? const Icon(Icons.arrow_back_ios_rounded) - : const Icon(Icons.close_rounded), + icon: + !selectionEnabledHook.value ? const Icon(Icons.arrow_back_ios_rounded) : const Icon(Icons.close_rounded), ), centerTitle: !selectionEnabledHook.value, automaticallyImplyLeading: false, @@ -192,9 +182,7 @@ class TrashPage extends HookConsumerWidget { color: Colors.red[400], ), label: Text( - selection.value.isEmpty - ? 'trash_page_delete_all'.tr() - : 'delete'.tr(), + selection.value.isEmpty ? 'trash_page_delete_all'.tr() : 'delete'.tr(), style: TextStyle( fontSize: 14, color: Colors.red[400], @@ -212,9 +200,7 @@ class TrashPage extends HookConsumerWidget { Icons.history_rounded, ), label: Text( - selection.value.isEmpty - ? 'trash_page_restore_all'.tr() - : 'restore'.tr(), + selection.value.isEmpty ? 'trash_page_restore_all'.tr() : 'restore'.tr(), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, diff --git a/mobile/lib/pages/onboarding/permission_onboarding.page.dart b/mobile/lib/pages/onboarding/permission_onboarding.page.dart index b0a1b34b06..0233208585 100644 --- a/mobile/lib/pages/onboarding/permission_onboarding.page.dart +++ b/mobile/lib/pages/onboarding/permission_onboarding.page.dart @@ -33,10 +33,8 @@ class PermissionOnboardingPage extends HookConsumerWidget { ).tr(), const SizedBox(height: 18), ElevatedButton( - onPressed: () => ref - .read(galleryPermissionNotifier.notifier) - .requestGalleryPermission() - .then((permission) async { + onPressed: () => + ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission().then((permission) async { if (permission.isGranted) { // If permission is limited, we will show the limited // permission page @@ -139,12 +137,8 @@ class PermissionOnboardingPage extends HookConsumerWidget { final Widget child = switch (permission) { PermissionStatus.limited => buildPermissionLimited(), PermissionStatus.denied => buildRequestPermission(), - PermissionStatus.granted || - PermissionStatus.provisional => - buildPermissionGranted(), - PermissionStatus.restricted || - PermissionStatus.permanentlyDenied => - buildPermissionDenied() + PermissionStatus.granted || PermissionStatus.provisional => buildPermissionGranted(), + PermissionStatus.restricted || PermissionStatus.permanentlyDenied => buildPermissionDenied() }; return Scaffold( diff --git a/mobile/lib/pages/photos/memory.page.dart b/mobile/lib/pages/photos/memory.page.dart index 211472f27a..4826e19f89 100644 --- a/mobile/lib/pages/photos/memory.page.dart +++ b/mobile/lib/pages/photos/memory.page.dart @@ -40,8 +40,7 @@ class MemoryPage extends HookConsumerWidget { final currentAsset = useState(null); /// The list of all of the asset page controllers - final memoryAssetPageControllers = - List.generate(memories.length, (i) => usePageController()); + final memoryAssetPageControllers = List.generate(memories.length, (i) => usePageController()); /// The main vertically scrolling page controller with each list of memories final memoryPageController = usePageController(initialPage: memoryIndex); @@ -73,19 +72,16 @@ class MemoryPage extends HookConsumerWidget { // Wait for the next frame to ensure the page is built SchedulerBinding.instance.addPostFrameCallback((_) { final previousIndex = currentMemoryIndex.value - 1; - final previousMemoryController = - memoryAssetPageControllers[previousIndex]; + final previousMemoryController = memoryAssetPageControllers[previousIndex]; // Ensure the controller is attached if (previousMemoryController.hasClients) { - previousMemoryController - .jumpToPage(memories[previousIndex].assets.length - 1); + 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); + previousMemoryController.jumpToPage(memories[previousIndex].assets.length - 1); } }); } @@ -96,8 +92,7 @@ class MemoryPage extends HookConsumerWidget { toNextAsset(int currentAssetIndex) { if (currentAssetIndex + 1 < currentMemory.value.assets.length) { // Go to the next asset - PageController controller = - memoryAssetPageControllers[currentMemoryIndex.value]; + PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; controller.nextPage( curve: Curves.easeInOut, @@ -112,8 +107,7 @@ class MemoryPage extends HookConsumerWidget { toPreviousAsset(int currentAssetIndex) { if (currentAssetIndex > 0) { // Go to the previous asset - PageController controller = - memoryAssetPageControllers[currentMemoryIndex.value]; + PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; controller.previousPage( curve: Curves.easeInOut, @@ -126,8 +120,7 @@ class MemoryPage extends HookConsumerWidget { } updateProgressText() { - assetProgress.value = - "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"; + assetProgress.value = "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"; } /// Downloads and caches the image for the asset at this [currentMemory]'s index @@ -180,8 +173,7 @@ class MemoryPage extends HookConsumerWidget { // 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.delayed(const Duration(milliseconds: 200)).then((_) => precacheAsset(1)); } Future onAssetChanged(int otherIndex) async { @@ -212,12 +204,10 @@ class MemoryPage extends HookConsumerWidget { // 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 isEpiloguePage = (memoryPageController.page?.floor() ?? 0) >= memories.length; final offset = notification.metrics.pixels; - if (isEpiloguePage && - (offset > notification.metrics.maxScrollExtent + 150)) { + if (isEpiloguePage && (offset > notification.metrics.maxScrollExtent + 150)) { context.maybePop(); return true; } @@ -358,8 +348,7 @@ class MemoryPage extends HookConsumerWidget { ), ), ), - if (currentAsset.value != null && - currentAsset.value!.isVideo) + if (currentAsset.value != null && currentAsset.value!.isVideo) Positioned( bottom: 24, right: 32, diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart index 62ac96c8aa..2cc3c44e3a 100644 --- a/mobile/lib/pages/photos/photos.page.dart +++ b/mobile/lib/pages/photos/photos.page.dart @@ -106,9 +106,7 @@ class PhotosPage extends HookConsumerWidget { return Stack( children: [ MultiselectGrid( - topWidget: (currentUser != null && currentUser.memoryEnabled) - ? const MemoryLane() - : const SizedBox(), + topWidget: (currentUser != null && currentUser.memoryEnabled) ? const MemoryLane() : const SizedBox(), renderListProvider: timelineUsers.length > 1 ? multiUsersTimelineProvider(timelineUsers) : singleUserTimelineProvider(currentUser?.id), @@ -120,9 +118,7 @@ class PhotosPage extends HookConsumerWidget { ), AnimatedPositioned( duration: const Duration(milliseconds: 300), - top: ref.watch(multiselectProvider) - ? -(kToolbarHeight + context.padding.top) - : 0, + top: ref.watch(multiselectProvider) ? -(kToolbarHeight + context.padding.top) : 0, left: 0, right: 0, child: Container( diff --git a/mobile/lib/pages/search/all_people.page.dart b/mobile/lib/pages/search/all_people.page.dart index 7e2a136721..6fcf07fbfa 100644 --- a/mobile/lib/pages/search/all_people.page.dart +++ b/mobile/lib/pages/search/all_people.page.dart @@ -28,9 +28,7 @@ class AllPeoplePage extends HookConsumerWidget { body: curatedPeople.widgetWhen( onData: (people) => ExploreGrid( isPeople: true, - curatedContent: people - .map((e) => SearchCuratedContent(label: e.name, id: e.id)) - .toList(), + 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 index 92521d13cf..63908a5e2a 100644 --- a/mobile/lib/pages/search/all_places.page.dart +++ b/mobile/lib/pages/search/all_places.page.dart @@ -13,8 +13,7 @@ class AllPlacesPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - AsyncValue> places = - ref.watch(getAllPlacesProvider); + AsyncValue> places = ref.watch(getAllPlacesProvider); return Scaffold( appBar: AppBar( diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart index 022bf5da5f..62df4a50af 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -48,8 +48,7 @@ class MapPage extends HookConsumerWidget { final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1)); final isLoading = useProcessingOverlay(); final scrollController = useScrollController(); - final markerDebouncer = - useDebouncer(interval: const Duration(milliseconds: 800)); + final markerDebouncer = useDebouncer(interval: const Duration(milliseconds: 800)); final selectedAssets = useValueNotifier>({}); const mapZoomToAssetLevel = 12.0; @@ -64,8 +63,7 @@ class MapPage extends HookConsumerWidget { final bounds = await mapController.value!.getVisibleRegion(); final inBounds = markers.value .where( - (m) => - bounds.contains(LatLng(m.latLng.latitude, m.latLng.longitude)), + (m) => bounds.contains(LatLng(m.latLng.latitude, m.latLng.longitude)), ) .toList(); // Notify bottom sheet to update asset grid only when there are new assets @@ -101,8 +99,7 @@ class MapPage extends HookConsumerWidget { useEffect( () { - final currentAssetLink = - ref.read(currentAssetProvider.notifier).ref.keepAlive(); + final currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive(); loadMarkers(); return currentAssetLink.close; @@ -128,8 +125,7 @@ class MapPage extends HookConsumerWidget { MapMarker marker, { bool shouldAnimate = true, }) async { - final assetPoint = - await mapController.value!.toScreenLocation(marker.latLng); + final assetPoint = await mapController.value!.toScreenLocation(marker.latLng); selectedMarker.value = _AssetMarkerMeta( point: assetPoint, marker: marker, @@ -144,11 +140,9 @@ class MapPage extends HookConsumerWidget { if (mapController.value == null) { return; } - final latlngBound = - await mapController.value!.getBoundsFromPoint(point, 50); + final latlngBound = await mapController.value!.getBoundsFromPoint(point, 50); final marker = markersInBounds.value.firstWhereOrNull( - (m) => - latlngBound.contains(LatLng(m.latLng.latitude, m.latLng.longitude)), + (m) => latlngBound.contains(LatLng(m.latLng.latitude, m.latLng.longitude)), ); if (marker != null) { @@ -156,7 +150,7 @@ class MapPage extends HookConsumerWidget { } else { // If no asset was previously selected and no new asset is available, close the bottom sheet if (selectedMarker.value == null) { - bottomSheetStreamController.add(MapCloseBottomSheet()); + bottomSheetStreamController.add(const MapCloseBottomSheet()); } selectedMarker.value = null; } @@ -211,16 +205,14 @@ class MapPage extends HookConsumerWidget { } void onBottomSheetScrolled(String assetRemoteId) { - final assetMarker = markersInBounds.value - .firstWhereOrNull((m) => m.assetRemoteId == 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); + 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; @@ -236,8 +228,7 @@ class MapPage extends HookConsumerWidget { } void onZoomToLocation() async { - final (location, error) = - await MapUtils.checkPermAndGetLocation(context: context); + final (location, error) = await MapUtils.checkPermAndGetLocation(context: context); if (error != null) { if (error == LocationPermission.unableToDetermine && context.mounted) { ImmichToast.show( @@ -360,8 +351,7 @@ class _AssetMarkerMeta { }); @override - String toString() => - '_AssetMarkerMeta(point: $point, marker: $marker, shouldAnimate: $shouldAnimate)'; + String toString() => '_AssetMarkerMeta(point: $point, marker: $marker, shouldAnimate: $shouldAnimate)'; } class _MapWithMarker extends StatelessWidget { diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart index f27deae052..9b7bbd0cd8 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -35,8 +35,7 @@ class MapLocationPickerPage extends HookConsumerWidget { selectedLatLng.value = centre; controller.value?.animateCamera(CameraUpdate.newLatLng(centre)); if (marker.value != null) { - await controller.value - ?.updateSymbol(marker.value!, SymbolOptions(geometry: centre)); + await controller.value?.updateSymbol(marker.value!, SymbolOptions(geometry: centre)); } } @@ -45,15 +44,13 @@ class MapLocationPickerPage extends HookConsumerWidget { } Future getCurrentLocation() async { - var (currentLocation, _) = - await MapUtils.checkPermAndGetLocation(context: context); + var (currentLocation, _) = await MapUtils.checkPermAndGetLocation(context: context); if (currentLocation == null) { return; } - var currentLatLng = - LatLng(currentLocation.latitude, currentLocation.longitude); + var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude); selectedLatLng.value = currentLatLng; controller.value?.animateCamera(CameraUpdate.newLatLng(currentLatLng)); } @@ -75,11 +72,9 @@ class MapLocationPickerPage extends HookConsumerWidget { ), ), child: MapLibreMap( - initialCameraPosition: - CameraPosition(target: initialLatLng, zoom: 12), + initialCameraPosition: CameraPosition(target: initialLatLng, zoom: 12), styleString: style, - onMapCreated: (mapController) => - controller.value = mapController, + onMapCreated: (mapController) => controller.value = mapController, onStyleLoadedCallback: onStyleLoaded, onMapClick: onMapClick, dragEnabled: false, @@ -165,8 +160,7 @@ class _BottomBar extends StatelessWidget { children: [ ElevatedButton( onPressed: onUseLocation, - child: - const Text("map_location_picker_page_use_location").tr(), + child: const Text("map_location_picker_page_use_location").tr(), ), ElevatedButton( onPressed: onGetCurrentLocation, diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 0ab94df7c8..d9a20e8b4d 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -48,8 +48,7 @@ class SearchPage extends HookConsumerWidget { isFavorite: false, ), mediaType: prefilter?.mediaType ?? AssetType.other, - language: - "${context.locale.languageCode}-${context.locale.countryCode}", + language: "${context.locale.languageCode}-${context.locale.countryCode}", ), ); @@ -87,9 +86,7 @@ class SearchPage extends HookConsumerWidget { isSearching.value = true; ref.watch(paginatedSearchProvider.notifier).clear(); - final hasResult = await ref - .watch(paginatedSearchProvider.notifier) - .search(filter.value); + final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value); if (!hasResult) { context.showSnackBar( @@ -103,9 +100,7 @@ class SearchPage extends HookConsumerWidget { loadMoreSearchResult() async { isSearching.value = true; - final hasResult = await ref - .watch(paginatedSearchProvider.notifier) - .search(filter.value); + final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value); if (!hasResult) { context.showSnackBar( @@ -147,7 +142,7 @@ class SearchPage extends HookConsumerWidget { ); showPeoplePicker() { - handleOnSelect(Set value) { + handleOnSelect(Set value) { filter.value = filter.value.copyWith( people: value, ); @@ -416,8 +411,7 @@ class SearchPage extends HookConsumerWidget { ), ); if (value) { - filterText - .add('search_filter_display_option_not_in_album'.tr()); + filterText.add('search_filter_display_option_not_in_album'.tr()); } break; case DisplayOption.archive: @@ -511,16 +505,11 @@ class SearchPage extends HookConsumerWidget { search(); } - IconData getSearchPrefixIcon() { - switch (textSearchType.value) { - case TextSearchType.context: - return Icons.image_search_rounded; - case TextSearchType.filename: - return Icons.abc_rounded; - case TextSearchType.description: - return Icons.text_snippet_outlined; - } - } + IconData getSearchPrefixIcon() => switch (textSearchType.value) { + TextSearchType.context => Icons.image_search_rounded, + TextSearchType.filename => Icons.abc_rounded, + TextSearchType.description => Icons.text_snippet_outlined, + }; return Scaffold( resizeToAvoidBottomInset: false, @@ -533,8 +522,10 @@ class SearchPage extends HookConsumerWidget { style: MenuStyle( elevation: const WidgetStatePropertyAll(1), shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), + const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(24), + ), ), ), padding: const WidgetStatePropertyAll( @@ -566,9 +557,7 @@ class SearchPage extends HookConsumerWidget { 'search_by_context'.tr(), style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.context - ? context.colorScheme.primary - : null, + color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null, ), ), selectedColor: context.colorScheme.primary, @@ -586,9 +575,7 @@ class SearchPage extends HookConsumerWidget { 'search_filter_filename'.tr(), style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.filename - ? context.colorScheme.primary - : null, + color: textSearchType.value == TextSearchType.filename ? context.colorScheme.primary : null, ), ), selectedColor: context.colorScheme.primary, @@ -606,15 +593,11 @@ class SearchPage extends HookConsumerWidget { 'search_by_description'.tr(), style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, - color: - textSearchType.value == TextSearchType.description - ? context.colorScheme.primary - : null, + color: textSearchType.value == TextSearchType.description ? context.colorScheme.primary : null, ), ), selectedColor: context.colorScheme.primary, - selected: - textSearchType.value == TextSearchType.description, + selected: textSearchType.value == TextSearchType.description, ), onPressed: () { textSearchType.value = TextSearchType.description; @@ -631,7 +614,9 @@ class SearchPage extends HookConsumerWidget { color: context.colorScheme.onSurface.withAlpha(0), width: 0, ), - borderRadius: BorderRadius.circular(24), + borderRadius: const BorderRadius.all( + Radius.circular(24), + ), gradient: LinearGradient( colors: [ context.colorScheme.primary.withValues(alpha: 0.075), @@ -646,9 +631,7 @@ class SearchPage 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), + contentPadding: prefilter != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8), prefixIcon: prefilter != null ? null : Icon( @@ -745,17 +728,13 @@ class SearchResultGrid extends StatelessWidget { padding: const EdgeInsets.only(top: 8.0), child: NotificationListener( onNotification: (notification) { - final isBottomSheetNotification = notification.context - ?.findAncestorWidgetOfExactType< - DraggableScrollableSheet>() != - null; + final isBottomSheetNotification = + notification.context?.findAncestorWidgetOfExactType() != null; final metrics = notification.metrics; final isVerticalScroll = metrics.axis == Axis.vertical; - if (metrics.pixels >= metrics.maxScrollExtent && - isVerticalScroll && - !isBottomSheetNotification) { + if (metrics.pixels >= metrics.maxScrollExtent && isVerticalScroll && !isBottomSheetNotification) { onScrollEnd(); } @@ -771,9 +750,7 @@ class SearchResultGrid extends StatelessWidget { dragScrollLabelEnabled: false, emptyIndicator: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: !isSearching - ? const SearchEmptyContent() - : const SizedBox.shrink(), + child: !isSearching ? const SearchEmptyContent() : const SizedBox.shrink(), ), ), ), @@ -795,9 +772,7 @@ class SearchEmptyContent extends StatelessWidget { const SizedBox(height: 40), Center( child: Image.asset( - context.isDarkTheme - ? 'assets/polaroid-dark.png' - : 'assets/polaroid-light.png', + context.isDarkTheme ? 'assets/polaroid-dark.png' : 'assets/polaroid-light.png', height: 125, ), ), @@ -823,7 +798,9 @@ class QuickLinkList extends StatelessWidget { Widget build(BuildContext context) { return Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), border: Border.all( color: context.colorScheme.outline.withAlpha(10), width: 1, diff --git a/mobile/lib/pages/settings/beta_sync_settings.page.dart b/mobile/lib/pages/settings/beta_sync_settings.page.dart new file mode 100644 index 0000000000..ba23ccf5eb --- /dev/null +++ b/mobile/lib/pages/settings/beta_sync_settings.page.dart @@ -0,0 +1,29 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/widgets/settings/beta_sync_settings/beta_sync_settings.dart'; + +@RoutePage() +class BetaSyncSettingsPage extends StatelessWidget { + const BetaSyncSettingsPage({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + title: const Text("beta_sync").t(context: context), + leading: IconButton( + onPressed: () => context.maybePop(true), + splashRadius: 24, + icon: const Icon( + Icons.arrow_back_ios_rounded, + ), + ), + ), + body: const BetaSyncSettings(), + ); + } +} diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index 3ff1b0c8ce..2b328b7ede 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -3,6 +3,7 @@ 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/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; @@ -37,9 +38,7 @@ class ShareIntentPage extends HookConsumerWidget { void upload() async { for (final attachment in candidates) { - await ref - .read(shareIntentUploadProvider.notifier) - .upload(attachment.file); + await ref.read(shareIntentUploadProvider.notifier).upload(attachment.file); } isUploaded.value = true; @@ -75,7 +74,7 @@ class ShareIntentPage extends HookConsumerWidget { leading: IconButton( onPressed: () { context.navigateTo( - const TabControllerRoute(), + Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute(), ); }, icon: const Icon(Icons.arrow_back), @@ -167,9 +166,7 @@ class ShareIntentPage extends HookConsumerWidget { height: 48, child: ElevatedButton( onPressed: isUploaded.value ? null : upload, - child: isUploaded.value - ? UploadingText(candidates: candidates) - : const Text('upload').tr(), + child: isUploaded.value ? UploadingText(candidates: candidates) : const Text('upload').tr(), ), ), ), @@ -265,7 +262,7 @@ class UploadStatusIcon extends StatelessWidget { color: Colors.red, semanticLabel: 'canceled'.tr(), ), - UploadStatus.waitingtoRetry || UploadStatus.paused => Icon( + UploadStatus.waitingToRetry || UploadStatus.paused => Icon( Icons.pause_circle_rounded, color: context.primaryColor, semanticLabel: 'paused'.tr(), diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart index dd6e545f88..b4d1d91b69 100644 --- a/mobile/lib/platform/native_sync_api.g.dart +++ b/mobile/lib/platform/native_sync_api.g.dart @@ -17,15 +17,12 @@ PlatformException _createConnectionError(String channelName) { bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { - return a.length == b.length && - a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + 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])); + (b as Map).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key])); } return a == b; } @@ -40,6 +37,7 @@ class PlatformAsset { this.width, this.height, required this.durationInSeconds, + required this.orientation, }); String id; @@ -58,6 +56,8 @@ class PlatformAsset { int durationInSeconds; + int orientation; + List _toList() { return [ id, @@ -68,6 +68,7 @@ class PlatformAsset { width, height, durationInSeconds, + orientation, ]; } @@ -86,6 +87,7 @@ class PlatformAsset { width: result[5] as int?, height: result[6] as int?, durationInSeconds: result[7]! as int, + orientation: result[8]! as int, ); } @@ -202,8 +204,7 @@ class SyncDelta { hasChanges: result[0]! as bool, updates: (result[1] as List?)!.cast(), deletes: (result[2] as List?)!.cast(), - assetAlbums: - (result[3] as Map?)!.cast>(), + assetAlbums: (result[3] as Map?)!.cast>(), ); } @@ -264,11 +265,9 @@ class NativeSyncApi { /// Constructor for [NativeSyncApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - NativeSyncApi( - {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + NativeSyncApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = - messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -278,15 +277,13 @@ class NativeSyncApi { Future shouldFullSync() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -308,15 +305,13 @@ class NativeSyncApi { Future getMediaChanges() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -338,15 +333,13 @@ class NativeSyncApi { Future checkpointSync() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -363,15 +356,13 @@ class NativeSyncApi { Future clearSyncCheckpoint() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -388,16 +379,13 @@ class NativeSyncApi { Future> getAssetIdsForAlbum(String albumId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = - pigeonVar_channel.send([albumId]); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; + 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) { @@ -419,15 +407,13 @@ class NativeSyncApi { Future> getAlbums() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -449,16 +435,13 @@ class NativeSyncApi { Future getAssetsCountSince(String albumId, int timestamp) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = - pigeonVar_channel.send([albumId, timestamp]); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; + 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) { @@ -477,20 +460,16 @@ class NativeSyncApi { } } - Future> getAssetsForAlbum(String albumId, - {int? updatedTimeCond}) async { + Future> getAssetsForAlbum(String albumId, {int? updatedTimeCond}) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = - pigeonVar_channel.send([albumId, updatedTimeCond]); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; + 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) { @@ -512,16 +491,13 @@ class NativeSyncApi { Future> hashPaths(List paths) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashPaths$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = - pigeonVar_channel.send([paths]); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; + final Future pigeonVar_sendFuture = pigeonVar_channel.send([paths]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index 6fbb83185e..439998065c 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -5,41 +5,81 @@ import 'package:drift/drift.dart' hide Column; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; final _features = [ + _Feature( + name: 'Main Timeline', + icon: Icons.timeline_rounded, + onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()), + ), + _Feature( + name: 'Selection Mode Timeline', + icon: Icons.developer_mode_rounded, + onTap: (ctx, ref) async { + final user = ref.watch(currentUserProvider); + if (user == null) { + return Future.value(); + } + + final assets = await ref.read(remoteAssetRepositoryProvider).getSome(user.id); + + final selectedAssets = await ctx.pushRoute>( + DriftAssetSelectionTimelineRoute( + lockedSelectionAssets: assets.toSet(), + ), + ); + + DLog.log( + "Selected ${selectedAssets?.length ?? 0} assets", + ); + + return Future.value(); + }, + ), + _Feature( + name: '', + icon: Icons.vertical_align_center_sharp, + onTap: (_, __) => Future.value(), + ), _Feature( name: 'Sync Local', icon: Icons.photo_album_rounded, onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(), ), _Feature( - name: 'Sync Local Full', + name: 'Sync Local Full (1)', icon: Icons.photo_library_rounded, onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true), ), _Feature( - name: 'Hash Local Assets', + name: 'Hash Local Assets (2)', icon: Icons.numbers_outlined, onTap: (_, ref) => ref.read(backgroundSyncProvider).hashAssets(), ), _Feature( - name: 'Sync Remote', + name: 'Sync Remote (3)', icon: Icons.refresh_rounded, onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(), ), _Feature( name: 'WAL Checkpoint', icon: Icons.save_rounded, - onTap: (_, ref) => ref - .read(driftProvider) - .customStatement("pragma wal_checkpoint(truncate)"), + onTap: (_, ref) => ref.read(driftProvider).customStatement("pragma wal_checkpoint(truncate)"), + ), + _Feature( + name: '', + icon: Icons.vertical_align_center_sharp, + onTap: (_, __) => Future.value(), ), _Feature( name: 'Clear Delta Checkpoint', @@ -48,6 +88,10 @@ final _features = [ ), _Feature( name: 'Clear Local Data', + style: const TextStyle( + color: Colors.orange, + fontWeight: FontWeight.bold, + ), icon: Icons.delete_forever_rounded, onTap: (_, ref) async { final db = ref.read(driftProvider); @@ -58,26 +102,50 @@ final _features = [ ), _Feature( name: 'Clear Remote Data', + style: const TextStyle( + color: Colors.orange, + fontWeight: FontWeight.bold, + ), icon: Icons.delete_sweep_rounded, onTap: (_, ref) async { final db = ref.read(driftProvider); await db.remoteAssetEntity.deleteAll(); await db.remoteExifEntity.deleteAll(); + await db.remoteAlbumEntity.deleteAll(); + await db.remoteAlbumUserEntity.deleteAll(); + await db.remoteAlbumAssetEntity.deleteAll(); + await db.memoryEntity.deleteAll(); + await db.memoryAssetEntity.deleteAll(); + await db.stackEntity.deleteAll(); + await db.personEntity.deleteAll(); + await db.assetFaceEntity.deleteAll(); }, ), _Feature( name: 'Local Media Summary', + style: const TextStyle( + color: Colors.indigo, + fontWeight: FontWeight.bold, + ), icon: Icons.table_chart_rounded, onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()), ), _Feature( name: 'Remote Media Summary', + style: const TextStyle( + color: Colors.indigo, + fontWeight: FontWeight.bold, + ), icon: Icons.summarize_rounded, onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()), ), _Feature( name: 'Reset Sqlite', icon: Icons.table_view_rounded, + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + ), onTap: (_, ref) async { final drift = ref.read(driftProvider); // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member @@ -88,11 +156,6 @@ final _features = [ } }, ), - _Feature( - name: 'Main Timeline', - icon: Icons.timeline_rounded, - onTap: (ctx, _) => ctx.pushRoute(const MainTimelineRoute()), - ), ]; @RoutePage() @@ -115,7 +178,10 @@ class FeatInDevPage extends StatelessWidget { final feat = _features[index]; return Consumer( builder: (ctx, ref, _) => ListTile( - title: Text(feat.name), + title: Text( + feat.name, + style: feat.style, + ), trailing: Icon(feat.icon), visualDensity: VisualDensity.compact, onTap: () => unawaited(feat.onTap(ctx, ref)), @@ -138,10 +204,12 @@ class _Feature { required this.name, required this.icon, required this.onTap, + this.style, }); final String name; final IconData icon; + final TextStyle? style; final Future Function(BuildContext, WidgetRef _) onTap; } diff --git a/mobile/lib/presentation/pages/dev/local_timeline.page.dart b/mobile/lib/presentation/pages/dev/local_timeline.page.dart deleted file mode 100644 index 8c06a6b62a..0000000000 --- a/mobile/lib/presentation/pages/dev/local_timeline.page.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/widgets.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; - -@RoutePage() -class LocalTimelinePage extends StatelessWidget { - final String albumId; - - const LocalTimelinePage({super.key, required this.albumId}); - - @override - Widget build(BuildContext context) { - return ProviderScope( - overrides: [ - timelineServiceProvider.overrideWith( - (ref) { - final timelineService = - ref.watch(timelineFactoryProvider).localAlbum(albumId: albumId); - ref.onDispose(() => unawaited(timelineService.dispose())); - return timelineService; - }, - ), - ], - child: const Timeline(), - ); - } -} diff --git a/mobile/lib/presentation/pages/dev/main_timeline.page.dart b/mobile/lib/presentation/pages/dev/main_timeline.page.dart index 8c04f129eb..0582399eaf 100644 --- a/mobile/lib/presentation/pages/dev/main_timeline.page.dart +++ b/mobile/lib/presentation/pages/dev/main_timeline.page.dart @@ -1,31 +1,32 @@ -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/presentation/widgets/memory/memory_lane.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; @RoutePage() -class MainTimelinePage extends StatelessWidget { +class MainTimelinePage extends ConsumerWidget { const MainTimelinePage({super.key}); @override - Widget build(BuildContext context) { - return ProviderScope( - overrides: [ - timelineServiceProvider.overrideWith( - (ref) { - final timelineService = ref - .watch(timelineFactoryProvider) - .main(ref.watch(timelineUsersIdsProvider)); - ref.onDispose(() => unawaited(timelineService.dispose())); - return timelineService; - }, - ), - ], - child: const Timeline(), + Widget build(BuildContext context, WidgetRef ref) { + final memoryLaneProvider = ref.watch(driftMemoryFutureProvider); + + return memoryLaneProvider.maybeWhen( + data: (memories) { + return memories.isEmpty + ? const Timeline(showStorageIndicator: true) + : Timeline( + topSliverWidget: SliverToBoxAdapter( + key: Key('memory-lane-${memories.first.assets.first.id}'), + child: DriftMemoryLane(memories: memories), + ), + topSliverWidgetHeight: 200, + showStorageIndicator: true, + ); + }, + orElse: () => const Timeline(showStorageIndicator: true), ); } } diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index cc1fd0ae0c..70e93fd212 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -40,7 +40,10 @@ class _Summary extends StatelessWidget { } else if (snapshot.hasError) { subtitle = const Icon(Icons.error_rounded); } else { - subtitle = Text('${snapshot.data ?? 0}'); + subtitle = Text( + '${snapshot.data ?? 0}', + style: ctx.textTheme.bodyLarge, + ); } return ListTile( leading: leading, @@ -114,15 +117,14 @@ class LocalMediaSummaryPage extends StatelessWidget { return SliverList.builder( itemBuilder: (_, index) { final album = albums[index]; - final countFuture = db.managers.localAlbumAssetEntity - .filter((f) => f.albumId.id.equals(album.id)) - .count(); + final countFuture = + db.managers.localAlbumAssetEntity.filter((f) => f.albumId.id.equals(album.id)).count(); return _Summary( leading: const Icon(Icons.photo_album_rounded), name: album.name, countFuture: countFuture, onTap: () => context.router.push( - LocalTimelineRoute(albumId: album.id), + LocalTimelineRoute(album: album), ), ); }, @@ -147,6 +149,30 @@ final _remoteStats = [ name: 'Exif Entities', load: (db) => db.managers.remoteExifEntity.count(), ), + _Stat( + name: 'Remote Albums', + load: (db) => db.managers.remoteAlbumEntity.count(), + ), + _Stat( + name: 'Memories', + load: (db) => db.managers.memoryEntity.count(), + ), + _Stat( + name: 'Memories Assets', + load: (db) => db.managers.memoryAssetEntity.count(), + ), + _Stat( + name: 'Stacks', + load: (db) => db.managers.stackEntity.count(), + ), + _Stat( + name: 'People', + load: (db) => db.managers.personEntity.count(), + ), + _Stat( + name: 'AssetFaces', + load: (db) => db.managers.assetFaceEntity.count(), + ), ]; @RoutePage() @@ -160,6 +186,7 @@ class RemoteMediaSummaryPage extends StatelessWidget { body: Consumer( builder: (ctx, ref, __) { final db = ref.watch(driftProvider); + final albumsFuture = ref.watch(remoteAlbumRepository).getAll(); return CustomScrollView( slivers: [ @@ -171,6 +198,48 @@ class RemoteMediaSummaryPage extends StatelessWidget { }, itemCount: _remoteStats.length, ), + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(left: 15), + child: Text( + "Album summary", + style: ctx.textTheme.titleMedium, + ), + ), + ], + ), + ), + FutureBuilder( + future: albumsFuture, + builder: (_, snap) { + final albums = snap.data ?? []; + if (albums.isEmpty) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + + albums.sortBy((a) => a.name); + return SliverList.builder( + itemBuilder: (_, index) { + final album = albums[index]; + final countFuture = + db.managers.remoteAlbumAssetEntity.filter((f) => f.albumId.id.equals(album.id)).count(); + return _Summary( + leading: const Icon(Icons.photo_album_rounded), + name: album.name, + countFuture: countFuture, + onTap: () => context.router.push( + RemoteAlbumRoute(album: album), + ), + ); + }, + itemCount: albums.length, + ); + }, + ), ], ); }, diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart new file mode 100644 index 0000000000..e22c63ac29 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_album.page.dart @@ -0,0 +1,61 @@ +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/presentation/widgets/album/album_selector.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; + +@RoutePage() +class DriftAlbumsPage extends ConsumerStatefulWidget { + const DriftAlbumsPage({super.key}); + + @override + ConsumerState createState() => _DriftAlbumsPageState(); +} + +class _DriftAlbumsPageState extends ConsumerState { + Future onRefresh() async { + await ref.read(remoteAlbumProvider.notifier).refresh(); + } + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: onRefresh, + edgeOffset: 100, + child: CustomScrollView( + slivers: [ + ImmichSliverAppBar( + snap: false, + floating: false, + pinned: true, + actions: [ + IconButton( + icon: const Icon( + Icons.add_rounded, + size: 28, + ), + onPressed: () => context.pushRoute( + const DriftCreateAlbumRoute(), + ), + ), + ], + showUploadButton: false, + ), + AlbumSelector( + onAlbumSelected: (album) { + ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); + context.router.push( + RemoteAlbumRoute(album: album), + ); + }, + ), + ], + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_archive.page.dart b/mobile/lib/presentation/pages/drift_archive.page.dart new file mode 100644 index 0000000000..8a6f1607d0 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_archive.page.dart @@ -0,0 +1,41 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; + +@RoutePage() +class DriftArchivePage extends StatelessWidget { + const DriftArchivePage({super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to access archive'); + } + + final timelineService = ref.watch(timelineFactoryProvider).archive(user.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: 'archive'.t(context: context), + icon: Icons.archive_outlined, + ), + bottomSheet: const ArchiveBottomSheet(), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart b/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart new file mode 100644 index 0000000000..0e5631082b --- /dev/null +++ b/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart @@ -0,0 +1,49 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; + +@RoutePage() +class DriftAssetSelectionTimelinePage extends ConsumerWidget { + final Set lockedSelectionAssets; + const DriftAssetSelectionTimelinePage({ + super.key, + this.lockedSelectionAssets = const {}, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ProviderScope( + overrides: [ + multiSelectProvider.overrideWith( + () => MultiSelectNotifier( + MultiSelectState( + selectedAssets: {}, + lockedSelectionAssets: lockedSelectionAssets, + forceEnable: true, + ), + ), + ), + timelineServiceProvider.overrideWith( + (ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception( + 'User must be logged in to access asset selection timeline', + ); + } + + final timelineService = ref.watch(timelineFactoryProvider).remoteAssets(user.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: const Timeline(), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_create_album.page.dart b/mobile/lib/presentation/pages/drift_create_album.page.dart new file mode 100644 index 0000000000..ba7157b15d --- /dev/null +++ b/mobile/lib/presentation/pages/drift_create_album.page.dart @@ -0,0 +1,495 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; + +@RoutePage() +class DriftCreateAlbumPage extends ConsumerStatefulWidget { + const DriftCreateAlbumPage({super.key}); + + @override + ConsumerState createState() => _DriftCreateAlbumPageState(); +} + +class _DriftCreateAlbumPageState extends ConsumerState { + TextEditingController albumTitleController = TextEditingController(); + TextEditingController albumDescriptionController = TextEditingController(); + FocusNode albumTitleTextFieldFocusNode = FocusNode(); + FocusNode albumDescriptionTextFieldFocusNode = FocusNode(); + bool isAlbumTitleTextFieldFocus = false; + Set selectedAssets = {}; + + @override + void dispose() { + albumTitleController.dispose(); + albumDescriptionController.dispose(); + albumTitleTextFieldFocusNode.dispose(); + albumDescriptionTextFieldFocusNode.dispose(); + super.dispose(); + } + + bool get _canCreateAlbum => albumTitleController.text.isNotEmpty; + + String _getEffectiveTitle() { + return albumTitleController.text.isNotEmpty + ? albumTitleController.text + : 'create_album_page_untitled'.t(context: context); + } + + Widget _buildSliverAppBar() { + return SliverAppBar( + backgroundColor: context.scaffoldBackgroundColor, + elevation: 0, + automaticallyImplyLeading: false, + pinned: true, + snap: false, + floating: false, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(200.0), + child: SizedBox( + height: 200, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + buildTitleInputField(), + buildDescriptionInputField(), + if (selectedAssets.isNotEmpty) buildControlButton(), + ], + ), + ), + ), + ); + } + + Widget _buildContent() { + if (selectedAssets.isEmpty) { + return SliverList( + delegate: SliverChildListDelegate([ + _buildEmptyState(), + _buildSelectPhotosButton(), + ]), + ); + } else { + return _buildSelectedImageGrid(); + } + } + + Widget _buildEmptyState() { + return Padding( + padding: const EdgeInsets.only(top: 0, left: 18), + child: Text( + 'create_shared_album_page_share_add_assets', + style: context.textTheme.labelLarge, + ).t(), + ); + } + + Widget _buildSelectPhotosButton() { + return Padding( + padding: const EdgeInsets.all(16.0), + child: FilledButton.icon( + style: FilledButton.styleFrom( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + vertical: 24.0, + horizontal: 16.0, + ), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + ), + backgroundColor: context.colorScheme.surfaceContainerHigh, + ), + onPressed: onSelectPhotos, + 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, + ), + ).t(), + ), + ), + ); + } + + Widget _buildSelectedImageGrid() { + return SliverPadding( + padding: const EdgeInsets.only(top: 16.0), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 1.0, + mainAxisSpacing: 1.0, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + final asset = selectedAssets.elementAt(index); + return GestureDetector( + onTap: onBackgroundTapped, + child: Thumbnail(asset: asset), + ); + }, + childCount: selectedAssets.length, + ), + ), + ); + } + + void onBackgroundTapped() { + albumTitleTextFieldFocusNode.unfocus(); + albumDescriptionTextFieldFocusNode.unfocus(); + setState(() { + isAlbumTitleTextFieldFocus = false; + }); + + if (albumTitleController.text.isEmpty) { + final untitledText = 'create_album_page_untitled'.t(); + albumTitleController.text = untitledText; + } + } + + Future onSelectPhotos() async { + final assets = await context.pushRoute>( + DriftAssetSelectionTimelineRoute( + lockedSelectionAssets: selectedAssets, + ), + ); + + if (assets == null || assets.isEmpty) { + return; + } + + setState(() { + selectedAssets = selectedAssets.union(assets); + }); + } + + Future createAlbum() async { + onBackgroundTapped(); + + final title = _getEffectiveTitle().trim(); + if (title.isEmpty) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('create_album_title_required'.t()), + backgroundColor: context.colorScheme.error, + ), + ); + } + return; + } + + final album = await ref.watch(remoteAlbumProvider.notifier).createAlbum( + title: title, + description: albumDescriptionController.text.trim(), + assetIds: selectedAssets.map((asset) { + final remoteAsset = asset as RemoteAsset; + return remoteAsset.id; + }).toList(), + ); + + if (album != null) { + ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); + context.replaceRoute( + RemoteAlbumRoute(album: album), + ); + } + } + + Widget buildTitleInputField() { + return Padding( + padding: const EdgeInsets.only( + right: 10.0, + left: 10.0, + ), + child: _AlbumTitleTextField( + focusNode: albumTitleTextFieldFocusNode, + textController: albumTitleController, + isFocus: isAlbumTitleTextFieldFocus, + onFocusChanged: (focus) { + setState(() { + isAlbumTitleTextFieldFocus = focus; + }); + }, + ), + ); + } + + Widget buildDescriptionInputField() { + return Padding( + padding: const EdgeInsets.only( + right: 10.0, + left: 10.0, + top: 8, + ), + child: _AlbumViewerEditableDescription( + textController: albumDescriptionController, + focusNode: albumDescriptionTextFieldFocusNode, + ), + ); + } + + Widget buildControlButton() { + return Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 8.0, + bottom: 8.0, + ), + child: SizedBox( + height: 42.0, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + AlbumActionFilledButton( + iconData: Icons.add_photo_alternate_outlined, + onPressed: onSelectPhotos, + labelText: "add_photos".t(), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + centerTitle: false, + backgroundColor: context.scaffoldBackgroundColor, + leading: IconButton( + onPressed: () => context.maybePop(), + icon: const Icon(Icons.close_rounded), + ), + title: const Text('create_album').t(), + actions: [ + TextButton( + onPressed: _canCreateAlbum ? createAlbum : null, + child: Text( + 'create'.t(), + style: TextStyle( + fontWeight: FontWeight.bold, + color: _canCreateAlbum ? context.primaryColor : context.themeData.disabledColor, + ), + ), + ), + ], + ), + body: GestureDetector( + onTap: onBackgroundTapped, + child: CustomScrollView( + slivers: [ + _buildSliverAppBar(), + _buildContent(), + ], + ), + ), + ); + } +} + +class _AlbumTitleTextField extends StatefulWidget { + const _AlbumTitleTextField({ + required this.focusNode, + required this.textController, + required this.isFocus, + required this.onFocusChanged, + }); + + final FocusNode focusNode; + final TextEditingController textController; + final bool isFocus; + final ValueChanged onFocusChanged; + + @override + State<_AlbumTitleTextField> createState() => _AlbumTitleTextFieldState(); +} + +class _AlbumTitleTextFieldState extends State<_AlbumTitleTextField> { + @override + void initState() { + super.initState(); + widget.focusNode.addListener(_onFocusChange); + } + + @override + void dispose() { + widget.focusNode.removeListener(_onFocusChange); + super.dispose(); + } + + void _onFocusChange() { + widget.onFocusChanged(widget.focusNode.hasFocus); + } + + @override + Widget build(BuildContext context) { + return TextField( + focusNode: widget.focusNode, + style: TextStyle( + fontSize: 28.0, + color: context.colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + controller: widget.textController, + onTap: () { + if (widget.textController.text == 'create_album_page_untitled'.t(context: context)) { + widget.textController.clear(); + } + }, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 16.0, + ), + suffixIcon: widget.textController.text.isNotEmpty && widget.isFocus + ? IconButton( + onPressed: () { + widget.textController.clear(); + }, + icon: Icon( + Icons.cancel_rounded, + color: context.primaryColor, + ), + splashRadius: 10.0, + ) + : null, + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.all( + Radius.circular(16.0), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: context.primaryColor.withValues(alpha: 0.3), + ), + borderRadius: const BorderRadius.all( + Radius.circular(16.0), + ), + ), + hintText: 'add_a_title'.t(), + hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( + fontSize: 28.0, + fontWeight: FontWeight.bold, + height: 1.2, + ), + focusColor: Colors.grey[300], + fillColor: context.colorScheme.surfaceContainerHigh, + filled: true, + ), + ); + } +} + +class _AlbumViewerEditableDescription extends StatefulWidget { + const _AlbumViewerEditableDescription({ + required this.textController, + required this.focusNode, + }); + + final TextEditingController textController; + final FocusNode focusNode; + + @override + State<_AlbumViewerEditableDescription> createState() => _AlbumViewerEditableDescriptionState(); +} + +class _AlbumViewerEditableDescriptionState extends State<_AlbumViewerEditableDescription> { + @override + void initState() { + super.initState(); + widget.focusNode.addListener(_onFocusModeChange); + widget.textController.addListener(_onTextChange); + } + + @override + void dispose() { + widget.focusNode.removeListener(_onFocusModeChange); + widget.textController.removeListener(_onTextChange); + super.dispose(); + } + + void _onFocusModeChange() { + setState(() { + if (!widget.focusNode.hasFocus && widget.textController.text.isEmpty) { + widget.textController.clear(); + } + }); + } + + void _onTextChange() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: TextField( + focusNode: widget.focusNode, + style: context.textTheme.bodyLarge, + maxLines: 3, + minLines: 1, + controller: widget.textController, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 16.0, + ), + suffixIcon: widget.focusNode.hasFocus && widget.textController.text.isNotEmpty + ? IconButton( + onPressed: () { + widget.textController.clear(); + }, + icon: Icon( + Icons.cancel_rounded, + color: context.primaryColor, + ), + splashRadius: 10.0, + ) + : null, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: context.colorScheme.outline.withValues(alpha: 0.3), + ), + borderRadius: const BorderRadius.all( + Radius.circular(16.0), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: context.primaryColor.withValues(alpha: 0.3), + ), + borderRadius: const BorderRadius.all( + Radius.circular(16.0), + ), + ), + hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( + fontSize: 16.0, + color: context.colorScheme.onSurface.withValues(alpha: 0.6), + ), + focusColor: Colors.grey[300], + fillColor: context.scaffoldBackgroundColor, + filled: widget.focusNode.hasFocus, + hintText: 'add_a_description'.t(), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_favorite.page.dart b/mobile/lib/presentation/pages/drift_favorite.page.dart new file mode 100644 index 0000000000..5648fd7fac --- /dev/null +++ b/mobile/lib/presentation/pages/drift_favorite.page.dart @@ -0,0 +1,41 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; + +@RoutePage() +class DriftFavoritePage extends StatelessWidget { + const DriftFavoritePage({super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to access favorite'); + } + + final timelineService = ref.watch(timelineFactoryProvider).favorite(user.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: 'favorites'.t(context: context), + icon: Icons.favorite_outline, + ), + bottomSheet: const FavoriteBottomSheet(), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart new file mode 100644 index 0000000000..5cf47bc995 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -0,0 +1,517 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/local_album_thumbnail.widget.dart'; +import 'package:immich_mobile/presentation/widgets/partner_user_avatar.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/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/services/api.service.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; +import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; + +@RoutePage() +class DriftLibraryPage extends ConsumerWidget { + const DriftLibraryPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return const Scaffold( + body: CustomScrollView( + slivers: [ + ImmichSliverAppBar( + snap: false, + floating: false, + pinned: true, + showUploadButton: false, + ), + _ActionButtonGrid(), + _CollectionCards(), + _QuickAccessButtonList(), + ], + ), + ); + } +} + +class _ActionButtonGrid extends ConsumerWidget { + const _ActionButtonGrid(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isTrashEnable = ref.watch( + serverInfoProvider.select((state) => state.serverFeatures.trash), + ); + + return SliverPadding( + padding: const EdgeInsets.only(left: 16, top: 16, right: 16, bottom: 12), + sliver: SliverToBoxAdapter( + child: Column( + children: [ + Row( + children: [ + _ActionButton( + icon: Icons.favorite_outline_rounded, + onTap: () => context.pushRoute(const DriftFavoriteRoute()), + label: 'favorites'.t(context: context), + ), + const SizedBox(width: 8), + _ActionButton( + icon: Icons.archive_outlined, + onTap: () => context.pushRoute(const DriftArchiveRoute()), + label: 'archived'.t(context: context), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + _ActionButton( + icon: Icons.link_outlined, + onTap: () => context.pushRoute(const SharedLinkRoute()), + label: 'shared_links'.t(context: context), + ), + isTrashEnable ? const SizedBox(width: 8) : const SizedBox.shrink(), + isTrashEnable + ? _ActionButton( + icon: Icons.delete_outline_rounded, + onTap: () => context.pushRoute(const DriftTrashRoute()), + label: 'trash'.t(context: context), + ) + : const SizedBox.shrink(), + ], + ), + ], + ), + ), + ); + } +} + +class _ActionButton extends StatelessWidget { + const _ActionButton({ + required this.icon, + required this.onTap, + required this.label, + }); + + final IconData icon; + final VoidCallback onTap; + final String label; + + @override + Widget build(BuildContext context) { + return Expanded( + child: FilledButton.icon( + onPressed: onTap, + 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, + ), + ), + ); + } +} + +class _CollectionCards extends StatelessWidget { + const _CollectionCards(); + + @override + Widget build(BuildContext context) { + return const SliverPadding( + padding: EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _PeopleCollectionCard(), + _PlacesCollectionCard(), + _LocalAlbumsCollectionCard(), + ], + ), + ), + ); + } +} + +class _PeopleCollectionCard extends ConsumerWidget { + const _PeopleCollectionCard(); + + @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: NetworkImage( + getFaceThumbnailUrl(person.id), + headers: ApiService.getRequestHeaders(), + ), + ); + }).toList(), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'people'.t(context: context), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _PlacesCollectionCard extends StatelessWidget { + const _PlacesCollectionCard(); + + @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( + DriftPlaceRoute(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( + 'places'.t(), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _LocalAlbumsCollectionCard extends ConsumerWidget { + const _LocalAlbumsCollectionCard(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final albums = ref.watch(localAlbumProvider); + + 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 DriftLocalAlbumsRoute()), + 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.when( + data: (data) { + return data.take(4).map((album) { + return LocalAlbumThumbnail( + albumId: album.id, + ); + }).toList(); + }, + error: (error, _) { + return [ + Center( + child: Text('Error: $error'), + ), + ]; + }, + loading: () { + return [ + const Center( + child: CircularProgressIndicator(), + ), + ]; + }, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'on_this_device'.t(context: context), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _QuickAccessButtonList extends ConsumerWidget { + const _QuickAccessButtonList(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final partnerSharedWithAsync = ref.watch(driftSharedWithPartnerProvider); + final partners = partnerSharedWithAsync.valueOrNull ?? []; + + return SliverPadding( + padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32), + sliver: SliverToBoxAdapter( + 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), + context.colorScheme.primary.withAlpha(20), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: ListView( + shrinkWrap: true, + padding: const EdgeInsets.all(0), + 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( + 'folders'.t(context: context), + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + onTap: () => context.pushRoute(FolderRoute()), + ), + ListTile( + leading: const Icon( + Icons.lock_outline_rounded, + size: 26, + ), + title: Text( + 'locked_folder'.t(context: context), + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + onTap: () => context.pushRoute(const DriftLockedFolderRoute()), + ), + ListTile( + leading: const Icon( + Icons.group_outlined, + size: 26, + ), + title: Text( + 'partners'.t(context: context), + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + onTap: () => context.pushRoute(const DriftPartnerRoute()), + ), + _PartnerList(partners: partners), + ], + ), + ), + ), + ); + } +} + +class _PartnerList extends StatelessWidget { + const _PartnerList({required this.partners}); + + final List partners; + + @override + Widget build(BuildContext context) { + return ListView.builder( + padding: const EdgeInsets.all(0), + 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: PartnerUserAvatar( + partner: partner, + ), + title: const Text( + "partner_list_user_photos", + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ).t(context: context, args: {'user': partner.name}), + onTap: () => context.pushRoute( + DriftPartnerDetailRoute(partner: partner), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_local_album.page.dart b/mobile/lib/presentation/pages/drift_local_album.page.dart new file mode 100644 index 0000000000..7add23259e --- /dev/null +++ b/mobile/lib/presentation/pages/drift_local_album.page.dart @@ -0,0 +1,115 @@ +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/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/presentation/widgets/images/local_album_thumbnail.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/common/local_album_sliver_app_bar.dart'; + +@RoutePage() +class DriftLocalAlbumsPage extends StatelessWidget { + const DriftLocalAlbumsPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: CustomScrollView( + slivers: [ + LocalAlbumsSliverAppBar(), + _AlbumList(), + ], + ), + ); + } +} + +class _AlbumList extends ConsumerWidget { + const _AlbumList(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final albums = ref.watch(localAlbumProvider); + + return albums.when( + loading: () => const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: CircularProgressIndicator(), + ), + ), + ), + error: (error, stack) => SliverToBoxAdapter( + child: Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Text( + 'Error loading albums: $error, stack: $stack', + style: TextStyle( + color: context.colorScheme.error, + ), + ), + ), + ), + ), + data: (albums) { + if (albums.isEmpty) { + return const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: Text('No albums found'), + ), + ), + ); + } + + return SliverPadding( + padding: const EdgeInsets.all(18.0), + sliver: SliverList.builder( + itemBuilder: (_, index) { + final album = albums[index]; + + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: LargeLeadingTile( + leadingPadding: const EdgeInsets.only( + right: 16, + ), + leading: SizedBox( + width: 80, + height: 80, + child: LocalAlbumThumbnail( + albumId: album.id, + ), + ), + title: Text( + album.name, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + 'items_count'.t( + context: context, + args: {'count': album.assetCount}, + ), + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), + ), + onTap: () => context.pushRoute(LocalTimelineRoute(album: album)), + ), + ); + }, + itemCount: albums.length, + ), + ); + }, + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_locked_folder.page.dart b/mobile/lib/presentation/pages/drift_locked_folder.page.dart new file mode 100644 index 0000000000..417e902de3 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_locked_folder.page.dart @@ -0,0 +1,74 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; + +@RoutePage() +class DriftLockedFolderPage extends ConsumerStatefulWidget { + const DriftLockedFolderPage({super.key}); + + @override + ConsumerState createState() => _DriftLockedFolderPageState(); +} + +class _DriftLockedFolderPageState extends ConsumerState with WidgetsBindingObserver { + bool _showOverlay = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (mounted) { + setState(() { + _showOverlay = state != AppLifecycleState.resumed; + }); + } + } + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to access locked folder'); + } + + final timelineService = ref.watch(timelineFactoryProvider).lockedFolder(user.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: _showOverlay + ? const SizedBox() + : PopScope( + onPopInvokedWithResult: (didPop, _) => didPop ? ref.read(authProvider.notifier).lockPinCode() : null, + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: 'locked_folder'.t(context: context), + ), + bottomSheet: const LockedFolderBottomSheet(), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_memory.page.dart b/mobile/lib/presentation/pages/drift_memory.page.dart new file mode 100644 index 0000000000..c42feff0ea --- /dev/null +++ b/mobile/lib/presentation/pages/drift_memory.page.dart @@ -0,0 +1,383 @@ +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/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/memory/memory_bottom_info.widget.dart'; +import 'package:immich_mobile/presentation/widgets/memory/memory_card.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/widgets/memories/memory_epilogue.dart'; +import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart'; + +/// Expects [currentAssetNotifier] to be set before navigating to this page +@RoutePage() +class DriftMemoryPage extends HookConsumerWidget { + final List memories; + final int memoryIndex; + + const DriftMemoryPage({ + 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 RemoteAsset 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( + getFullImageProvider( + asset, + size: Size(size.width, 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(currentAssetNotifier.notifier).setAsset(asset); + // if (asset.isVideo || asset.isMotionPhoto) { + if (asset.isVideo) { + ref.read(videoPlaybackValueProvider.notifier).reset(); + } + } + + /* 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, + ), + ); + } + + final yearsAgo = DateTime.now().year - memories[mIndex].data.year; + final title = 'years_ago'.t( + context: context, + args: { + 'years': yearsAgo.toString(), + }, + ); + // 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: DriftMemoryCard( + asset: asset, + title: 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], + ), + ), + ], + ), + ), + DriftMemoryBottomInfo( + memory: memories[mIndex], + title: title, + ), + ], + ); + }, + ), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_partner_detail.page.dart b/mobile/lib/presentation/pages/drift_partner_detail.page.dart new file mode 100644 index 0000000000..3d9d28aeab --- /dev/null +++ b/mobile/lib/presentation/pages/drift_partner_detail.page.dart @@ -0,0 +1,144 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; + +@RoutePage() +class DriftPartnerDetailPage extends StatelessWidget { + final PartnerUserDto partner; + + const DriftPartnerDetailPage({ + super.key, + required this.partner, + }); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final timelineService = ref.watch(timelineFactoryProvider).remoteAssets(partner.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: partner.name, + icon: Icons.person_outline, + ), + topSliverWidget: _InfoBox(partner: partner), + topSliverWidgetHeight: 110, + bottomSheet: const PartnerDetailBottomSheet(), + ), + ); + } +} + +class _InfoBox extends ConsumerStatefulWidget { + final PartnerUserDto partner; + + const _InfoBox({ + required this.partner, + }); + + @override + ConsumerState<_InfoBox> createState() => _InfoBoxState(); +} + +class _InfoBoxState extends ConsumerState<_InfoBox> { + bool _inTimeline = false; + + @override + void initState() { + super.initState(); + _inTimeline = widget.partner.inTimeline; + } + + _toggleInTimeline() async { + final user = ref.read(currentUserProvider); + if (user == null) { + return; + } + + try { + await ref.read(partnerUsersProvider.notifier).toggleShowInTimeline( + widget.partner.id, + user.id, + ); + + setState(() { + _inTimeline = !_inTimeline; + }); + } catch (error, stack) { + debugPrint("Failed to toggle in timeline: $error $stack"); + ImmichToast.show( + context: context, + toastType: ToastType.error, + durationInSecond: 1, + msg: "Failed to toggle the timeline setting", + ); + return; + } + } + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: SizedBox( + height: 110, + child: 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, + onChanged: (_) => _toggleInTimeline(), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_place.page.dart b/mobile/lib/presentation/pages/drift_place.page.dart new file mode 100644 index 0000000000..969bfc70fd --- /dev/null +++ b/mobile/lib/presentation/pages/drift_place.page.dart @@ -0,0 +1,195 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.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 DriftPlacePage extends StatelessWidget { + const DriftPlacePage({super.key, this.currentLocation}); + + final LatLng? currentLocation; + + @override + Widget build(BuildContext context) { + final ValueNotifier search = ValueNotifier(null); + + return Scaffold( + body: ValueListenableBuilder( + valueListenable: search, + builder: (context, searchValue, child) { + return CustomScrollView( + slivers: [ + _PlaceSliverAppBar(search: search), + _Map(search: search, currentLocation: currentLocation), + _PlaceList(search: search), + ], + ); + }, + ), + ); + } +} + +class _PlaceSliverAppBar extends StatelessWidget { + const _PlaceSliverAppBar({required this.search}); + + final ValueNotifier search; + + @override + Widget build(BuildContext context) { + final searchFocusNode = FocusNode(); + + return SliverAppBar( + floating: true, + pinned: true, + snap: false, + backgroundColor: context.colorScheme.surfaceContainer, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + automaticallyImplyLeading: search.value == null, + centerTitle: true, + title: search.value != null + ? SearchField( + focusNode: searchFocusNode, + onTapOutside: (_) => searchFocusNode.unfocus(), + onChanged: (value) => search.value = value, + filled: true, + hintText: 'filter_places'.t(context: context), + autofocus: true, + ) + : Text('places'.t(context: context)), + actions: [ + IconButton( + icon: Icon(search.value != null ? Icons.close : Icons.search), + onPressed: () { + search.value = search.value == null ? '' : null; + }, + ), + ], + ); + } +} + +class _Map extends StatelessWidget { + const _Map({required this.search, this.currentLocation}); + + final ValueNotifier search; + final LatLng? currentLocation; + + @override + Widget build(BuildContext context) { + return search.value == null + ? SliverPadding( + padding: const EdgeInsets.all(16.0), + sliver: SliverToBoxAdapter( + child: SizedBox( + height: 200, + width: context.width, + // TODO: migrate to DriftMapRoute after merging #19898 + 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, + ), + ), + ), + ) + : const SliverToBoxAdapter( + child: SizedBox.shrink(), + ); + } +} + +class _PlaceList extends ConsumerWidget { + const _PlaceList({required this.search}); + + final ValueNotifier search; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final places = ref.watch(placesProvider); + + return places.when( + loading: () => const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: CircularProgressIndicator(), + ), + ), + ), + error: (error, stack) => SliverToBoxAdapter( + child: Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Text( + 'Error loading places: $error, stack: $stack', + style: TextStyle( + color: context.colorScheme.error, + ), + ), + ), + ), + ), + data: (places) { + if (search.value != null) { + places = places.where((place) { + return place.$1.toLowerCase().contains(search.value!.toLowerCase()); + }).toList(); + } + + return SliverList.builder( + itemCount: places.length, + itemBuilder: (context, index) { + final place = places[index]; + return _PlaceTile(place: place); + }, + ); + }, + ); + } +} + +class _PlaceTile extends StatelessWidget { + const _PlaceTile({required this.place}); + + final (String, String) place; + + @override + Widget build(BuildContext context) { + return LargeLeadingTile( + onTap: () => context.pushRoute(DriftPlaceDetailRoute(place: place.$1)), + title: Text( + place.$1, + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + leading: ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), + child: Thumbnail( + size: const Size(80, 80), + fit: BoxFit.cover, + remoteId: place.$2, + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_place_detail.page.dart b/mobile/lib/presentation/pages/drift_place_detail.page.dart new file mode 100644 index 0000000000..d55725231f --- /dev/null +++ b/mobile/lib/presentation/pages/drift_place_detail.page.dart @@ -0,0 +1,37 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; + +@RoutePage() +class DriftPlaceDetailPage extends StatelessWidget { + final String place; + + const DriftPlaceDetailPage({ + super.key, + required this.place, + }); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final timelineService = ref.watch(timelineFactoryProvider).place(place); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: place, + icon: Icons.location_on, + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_recently_taken.page.dart b/mobile/lib/presentation/pages/drift_recently_taken.page.dart new file mode 100644 index 0000000000..c99c36bd24 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_recently_taken.page.dart @@ -0,0 +1,38 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; + +@RoutePage() +class DriftRecentlyTakenPage extends StatelessWidget { + const DriftRecentlyTakenPage({super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception( + 'User must be logged in to access recently taken', + ); + } + + final timelineService = ref.watch(timelineFactoryProvider).remoteAssets(user.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + appBar: MesmerizingSliverAppBar(title: 'recently_taken'.t()), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart new file mode 100644 index 0000000000..4173e262bc --- /dev/null +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -0,0 +1,440 @@ +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/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/remote_album.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/widgets/common/immich_toast.dart'; +import 'package:immich_mobile/widgets/common/remote_album_sliver_app_bar.dart'; + +@RoutePage() +class RemoteAlbumPage extends ConsumerStatefulWidget { + final RemoteAlbum album; + + const RemoteAlbumPage({ + super.key, + required this.album, + }); + + @override + ConsumerState createState() => _RemoteAlbumPageState(); +} + +class _RemoteAlbumPageState extends ConsumerState { + @override + void initState() { + super.initState(); + } + + Future addAssets(BuildContext context) async { + final albumAssets = await ref.read(remoteAlbumProvider.notifier).getAssets(widget.album.id); + + final newAssets = await context.pushRoute>( + DriftAssetSelectionTimelineRoute( + lockedSelectionAssets: albumAssets.toSet(), + ), + ); + + if (newAssets == null || newAssets.isEmpty) { + return; + } + + final added = await ref.read(remoteAlbumProvider.notifier).addAssets( + widget.album.id, + newAssets.map((asset) { + final remoteAsset = asset as RemoteAsset; + return remoteAsset.id; + }).toList(), + ); + + if (added > 0) { + ImmichToast.show( + context: context, + msg: "assets_added_to_album_count".t( + context: context, + args: { + 'count': added.toString(), + }, + ), + toastType: ToastType.success, + ); + } + } + + Future addUsers(BuildContext context) async { + final newUsers = await context.pushRoute>( + DriftUserSelectionRoute(album: widget.album), + ); + + if (newUsers == null || newUsers.isEmpty) { + return; + } + + try { + await ref.read(remoteAlbumProvider.notifier).addUsers(widget.album.id, newUsers); + + if (newUsers.isNotEmpty) { + ImmichToast.show( + context: context, + msg: "users_added_to_album_count".t( + context: context, + args: { + 'count': newUsers.length, + }, + ), + toastType: ToastType.success, + ); + } + + ref.invalidate(remoteAlbumSharedUsersProvider(widget.album.id)); + } catch (e) { + ImmichToast.show( + context: context, + msg: "Failed to add users to album: ${e.toString()}", + toastType: ToastType.error, + ); + } + } + + Future toggleAlbumOrder() async { + await ref.read(remoteAlbumProvider.notifier).toggleAlbumOrder( + widget.album.id, + ); + + ref.invalidate(timelineServiceProvider); + } + + Future deleteAlbum(BuildContext context) async { + final confirmed = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('delete_album'.t(context: context)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'album_delete_confirmation'.t( + context: context, + args: {'album': widget.album.name}, + ), + ), + const SizedBox(height: 8), + Text( + 'album_delete_confirmation_description'.t(context: context), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text('cancel'.t(context: context)), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.error, + ), + child: Text('delete_album'.t(context: context)), + ), + ], + ); + }, + ); + + if (confirmed == true) { + try { + await ref.read(remoteAlbumProvider.notifier).deleteAlbum(widget.album.id); + + ImmichToast.show( + context: context, + msg: 'album_deleted'.t(context: context), + toastType: ToastType.success, + ); + + context.pushRoute(const DriftAlbumsRoute()); + } catch (e) { + ImmichToast.show( + context: context, + msg: 'album_viewer_appbar_share_err_delete'.t(context: context), + toastType: ToastType.error, + ); + } + } + } + + Future showEditTitleAndDescription(BuildContext context) async { + final result = await showDialog<_EditAlbumData?>( + context: context, + barrierDismissible: true, + builder: (context) => _EditAlbumDialog(album: widget.album), + ); + + if (result != null && context.mounted) { + HapticFeedback.mediumImpact(); + } + } + + void showOptionSheet(BuildContext context) { + final user = ref.watch(currentUserProvider); + final isOwner = user != null ? user.id == widget.album.ownerId : false; + + showModalBottomSheet( + context: context, + backgroundColor: context.colorScheme.surface, + isScrollControlled: false, + builder: (context) { + return DriftRemoteAlbumOption( + onDeleteAlbum: isOwner + ? () async { + await deleteAlbum(context); + if (context.mounted) { + context.pop(); + } + } + : null, + onAddUsers: isOwner + ? () async { + await addUsers(context); + context.pop(); + } + : null, + onAddPhotos: () async { + await addAssets(context); + context.pop(); + }, + onToggleAlbumOrder: () async { + await toggleAlbumOrder(); + context.pop(); + }, + onEditAlbum: () async { + context.pop(); + await showEditTitleAndDescription(context); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final timelineService = ref.watch(timelineFactoryProvider).remoteAlbum(albumId: widget.album.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + appBar: RemoteAlbumSliverAppBar( + icon: Icons.photo_album_outlined, + onShowOptions: () => showOptionSheet(context), + onToggleAlbumOrder: () => toggleAlbumOrder(), + onEditTitle: () => showEditTitleAndDescription(context), + ), + bottomSheet: RemoteAlbumBottomSheet( + album: widget.album, + ), + ), + ); + } +} + +class _EditAlbumData { + final String name; + final String? description; + + const _EditAlbumData({ + required this.name, + this.description, + }); +} + +class _EditAlbumDialog extends ConsumerStatefulWidget { + final RemoteAlbum album; + + const _EditAlbumDialog({ + required this.album, + }); + + @override + ConsumerState<_EditAlbumDialog> createState() => _EditAlbumDialogState(); +} + +class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { + late final TextEditingController titleController; + late final TextEditingController descriptionController; + final formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + titleController = TextEditingController(text: widget.album.name); + descriptionController = TextEditingController( + text: widget.album.description.isEmpty ? '' : widget.album.description, + ); + } + + @override + void dispose() { + titleController.dispose(); + descriptionController.dispose(); + super.dispose(); + } + + Future _handleSave() async { + if (formKey.currentState?.validate() != true) return; + + try { + final newTitle = titleController.text.trim(); + final newDescription = descriptionController.text.trim(); + + await ref.read(remoteAlbumProvider.notifier).updateAlbum( + widget.album.id, + name: newTitle, + description: newDescription.isEmpty ? null : newDescription, + ); + + if (mounted) { + Navigator.of(context).pop( + _EditAlbumData( + name: newTitle, + description: newDescription.isEmpty ? null : newDescription, + ), + ); + } + } catch (e) { + if (mounted) { + ImmichToast.show( + context: context, + msg: 'album_update_error'.t(context: context), + toastType: ToastType.error, + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + insetPadding: const EdgeInsets.all(24), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(16), + ), + ), + child: SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(16), + constraints: const BoxConstraints(maxWidth: 550), + child: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Icon( + Icons.edit_outlined, + color: context.colorScheme.primary, + size: 24, + ), + const SizedBox(width: 12), + Text( + 'edit_album'.t(context: context), + style: context.textTheme.titleMedium, + ), + ], + ), + const SizedBox(height: 24), + + // Album Name + Text( + 'album_name'.t(context: context).toUpperCase(), + style: context.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + TextFormField( + controller: titleController, + maxLines: 1, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + filled: true, + fillColor: context.colorScheme.surface, + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'album_name_required'.t(context: context); + } + + return null; + }, + ), + const SizedBox(height: 18), + + // Description + Text( + 'description'.t(context: context).toUpperCase(), + style: context.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + TextFormField( + controller: descriptionController, + maxLines: 4, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + filled: true, + fillColor: context.colorScheme.surface, + ), + ), + const SizedBox(height: 24), + + // Action Buttons + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(null), + child: Text('cancel'.t(context: context)), + ), + const SizedBox(width: 12), + FilledButton( + onPressed: _handleSave, + child: Text('save'.t(context: context)), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_trash.page.dart b/mobile/lib/presentation/pages/drift_trash.page.dart new file mode 100644 index 0000000000..61fc5e35f7 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_trash.page.dart @@ -0,0 +1,66 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; + +@RoutePage() +class DriftTrashPage extends StatelessWidget { + const DriftTrashPage({super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to access trash'); + } + + final timelineService = ref.watch(timelineFactoryProvider).trash(user.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + showStorageIndicator: true, + appBar: SliverAppBar( + title: Text('trash'.t(context: context)), + floating: true, + snap: true, + pinned: true, + centerTitle: true, + elevation: 0, + ), + topSliverWidgetHeight: 24, + topSliverWidget: Consumer( + builder: (context, ref, child) { + final trashDays = ref.watch( + serverInfoProvider.select((v) => v.serverConfig.trashDays), + ); + + return SliverPadding( + padding: const EdgeInsets.all(16.0), + sliver: SliverToBoxAdapter( + child: SizedBox( + height: 24.0, + child: const Text( + "trash_page_info", + ).t(context: context, args: {"days": "$trashDays"}), + ), + ), + ); + }, + ), + bottomSheet: const TrashBottomBar(), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_user_selection.page.dart b/mobile/lib/presentation/pages/drift_user_selection.page.dart new file mode 100644 index 0000000000..5aaa438a11 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_user_selection.page.dart @@ -0,0 +1,208 @@ +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/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/user_metadata.model.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; + +// TODO: Refactor this provider when we have user provider/service/repository pattern in place +final driftUsersProvider = FutureProvider.autoDispose>((ref) async { + final drift = ref.watch(driftProvider); + final currentUser = ref.watch(currentUserProvider); + + final userEntities = await drift.managers.userEntity.get(); + + final users = userEntities + .map( + (entity) => UserDto( + id: entity.id, + name: entity.name, + email: entity.email, + isAdmin: entity.isAdmin, + profileImagePath: entity.profileImagePath, + updatedAt: entity.updatedAt, + quotaSizeInBytes: entity.quotaSizeInBytes ?? 0, + quotaUsageInBytes: entity.quotaUsageInBytes, + isPartnerSharedBy: false, + isPartnerSharedWith: false, + avatarColor: AvatarColor.primary, + memoryEnabled: true, + inTimeline: true, + ), + ) + .toList(); + + users.removeWhere((u) => currentUser?.id == u.id); + + return users; +}); + +@RoutePage() +class DriftUserSelectionPage extends HookConsumerWidget { + final RemoteAlbum album; + + const DriftUserSelectionPage({ + super.key, + required this.album, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final AsyncValue> suggestedShareUsers = ref.watch(driftUsersProvider); + 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) { + // Get shared users for this album from the database + final sharedUsers = ref.watch(remoteAlbumSharedUsersProvider(album.id)); + + return sharedUsers.when( + data: (albumSharedUsers) { + // Filter out users that are already shared with this album and the owner + final filteredUsers = users.where((user) { + return !albumSharedUsers.any((sharedUser) => sharedUser.id == user.id) && user.id != album.ownerId; + }).toList(); + + return buildUserList(filteredUsers); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) { + // If we can't load shared users, just filter out the owner + final filteredUsers = users.where((user) => user.id != album.ownerId).toList(); + return buildUserList(filteredUsers); + }, + ); + }, + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_video.page.dart b/mobile/lib/presentation/pages/drift_video.page.dart new file mode 100644 index 0000000000..94c5620f9a --- /dev/null +++ b/mobile/lib/presentation/pages/drift_video.page.dart @@ -0,0 +1,36 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; + +@RoutePage() +class DriftVideoPage extends StatelessWidget { + const DriftVideoPage({super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to video'); + } + + final timelineService = ref.watch(timelineFactoryProvider).video(user.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + appBar: MesmerizingSliverAppBar(title: 'videos'.t()), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/local_timeline.page.dart b/mobile/lib/presentation/pages/local_timeline.page.dart new file mode 100644 index 0000000000..d322b5d9d2 --- /dev/null +++ b/mobile/lib/presentation/pages/local_timeline.page.dart @@ -0,0 +1,35 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; + +@RoutePage() +class LocalTimelinePage extends StatelessWidget { + final LocalAlbum album; + + const LocalTimelinePage({super.key, required this.album}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final timelineService = ref.watch(timelineFactoryProvider).localAlbum(albumId: album.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + appBar: MesmerizingSliverAppBar(title: album.name), + bottomSheet: const LocalAlbumBottomSheet(), + showStorageIndicator: true, + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart new file mode 100644 index 0000000000..868f1ff298 --- /dev/null +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -0,0 +1,895 @@ +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/domain/models/timeline.model.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'; +import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; +import 'package:immich_mobile/routing/router.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 DriftSearchPage extends HookConsumerWidget { + const DriftSearchPage({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'.t(context: context)); + 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, + 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'.t(context: context)), + ); + } + + 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'.t(context: context)), + ); + } + + 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'.t(context: context)).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'.t(context: context), + 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'.t(context: context), + 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'.t(context: context), + 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'.t(context: context), + cancelText: 'cancel'.t(context: context), + confirmText: 'select'.t(context: context), + saveText: 'save'.t(context: context), + errorFormatText: 'invalid_date_format'.t(context: context), + errorInvalidText: 'invalid_date'.t(context: context), + fieldStartHintText: 'start_date'.t(context: context), + fieldEndHintText: 'end_date'.t(context: context), + initialEntryMode: DatePickerEntryMode.calendar, + keyboardType: TextInputType.text, + ); + + if (date == null) { + filter.value = filter.value.copyWith( + date: SearchDateFilter(), + ); + + dateRangeCurrentFilterWidget.value = null; + 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'.t( + context: context, + args: { + "start": DateFormat.yMMMd().format(date.start.toLocal()), + "end": DateFormat.yMMMd().format(date.end.toLocal()), + }, + ), + style: context.textTheme.labelLarge, + ); + } + + search(); + } + + // MEDIA PICKER + showMediaTypePicker() { + handleOnSelected(AssetType assetType) { + filter.value = filter.value.copyWith( + mediaType: assetType, + ); + + 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, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + mediaType: AssetType.other, + ); + + mediaTypeCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: 'search_filter_media_type_title'.t(context: context), + 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'.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, + ); + } + + 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'.t(context: context), + 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: '', + ); + + break; + case TextSearchType.filename: + filter.value = filter.value.copyWith( + filename: value, + context: '', + description: '', + ); + + break; + case TextSearchType.description: + filter.value = filter.value.copyWith( + filename: '', + context: '', + description: 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, + }; + + 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', + ); + }, + menuChildren: [ + MenuItemButton( + child: ListTile( + leading: const Icon(Icons.image_search_rounded), + title: Text( + 'search_by_context'.t(context: context), + 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'.t(context: context); + }, + ), + MenuItemButton( + child: ListTile( + leading: const Icon(Icons.abc_rounded), + title: Text( + 'search_filter_filename'.t(context: context), + 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'.t(context: context); + }, + ), + MenuItemButton( + child: ListTile( + leading: const Icon(Icons.text_snippet_outlined), + title: Text( + 'search_by_description'.t(context: context), + 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'.t(context: context); + }, + ), + ], + ), + ), + ], + 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: CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.only(top: 12.0, bottom: 4.0), + sliver: SliverToBoxAdapter( + 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'.t(context: context), + currentFilter: peopleCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.location_on_outlined, + onTap: showLocationPicker, + label: 'search_filter_location'.t(context: context), + currentFilter: locationCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.camera_alt_outlined, + onTap: showCameraPicker, + label: 'camera'.t(context: context), + currentFilter: cameraCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.date_range_outlined, + onTap: showDatePicker, + label: 'search_filter_date'.t(context: context), + currentFilter: dateRangeCurrentFilterWidget.value, + ), + SearchFilterChip( + key: const Key('media_type_chip'), + icon: Icons.video_collection_outlined, + onTap: showMediaTypePicker, + label: 'search_filter_media_type'.t(context: context), + currentFilter: mediaTypeCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.display_settings_outlined, + onTap: showDisplayOptionPicker, + label: 'search_filter_display_options'.t(context: context), + currentFilter: displayOptionCurrentFilterWidget.value, + ), + ], + ), + ), + ), + ), + if (isSearching.value) + const SliverFillRemaining( + hasScrollBody: false, + child: Center(child: CircularProgressIndicator()), + ) + else + _SearchResultGrid(onScrollEnd: loadMoreSearchResult), + ], + ), + ); + } +} + +class _SearchResultGrid extends ConsumerWidget { + final VoidCallback onScrollEnd; + + const _SearchResultGrid({required this.onScrollEnd}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final searchResult = ref.watch(paginatedSearchProvider); + + if (searchResult.totalAssets == 0) { + return const _SearchEmptyContent(); + } + + return 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: SliverFillRemaining( + child: ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final timelineService = ref.watch(timelineFactoryProvider).fromAssets(searchResult.assets); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + key: ValueKey(searchResult.totalAssets), + appBar: null, + groupBy: GroupAssetsBy.none, + ), + ), + ), + ); + } +} + +class _SearchEmptyContent extends StatelessWidget { + const _SearchEmptyContent(); + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: ListView( + shrinkWrap: true, + 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'.t(context: context), + style: context.textTheme.labelLarge, + ), + ), + const SizedBox(height: 32), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: _QuickLinkList(), + ), + ], + ), + ); + } +} + +class _QuickLinkList extends StatelessWidget { + const _QuickLinkList(); + + @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'.t(context: context), + icon: Icons.schedule_outlined, + isTop: true, + onTap: () => context.pushRoute(const DriftRecentlyTakenRoute()), + ), + _QuickLink( + title: 'videos'.t(context: context), + icon: Icons.play_circle_outline_rounded, + onTap: () => context.pushRoute(const DriftVideoRoute()), + ), + _QuickLink( + title: 'favorites'.t(context: context), + icon: Icons.favorite_border_rounded, + isBottom: true, + onTap: () => context.pushRoute(const DriftFavoriteRoute()), + ), + ], + ), + ); + } +} + +class _QuickLink extends StatelessWidget { + final String title; + final IconData icon; + final VoidCallback onTap; + final bool isTop; + final bool isBottom; + + const _QuickLink({ + 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/presentation/pages/search/paginated_search.provider.dart b/mobile/lib/presentation/pages/search/paginated_search.provider.dart new file mode 100644 index 0000000000..c93d002b95 --- /dev/null +++ b/mobile/lib/presentation/pages/search/paginated_search.provider.dart @@ -0,0 +1,38 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/search_result.model.dart'; +import 'package:immich_mobile/domain/services/search.service.dart'; +import 'package:immich_mobile/models/search/search_filter.model.dart'; +import 'package:immich_mobile/providers/infrastructure/search.provider.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); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart new file mode 100644 index 0000000000..f17bf553ce --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class ArchiveActionButton extends ConsumerWidget { + final ActionSource source; + + const ArchiveActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).archive(source); + ref.read(multiSelectProvider.notifier).reset(); + + if (source == ActionSource.viewer) { + EventStream.shared.emit(const ViewerReloadAssetEvent()); + } + + final successMessage = 'archive_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.archive_outlined, + label: "archive".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart new file mode 100644 index 0000000000..9704c4b13b --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class BaseActionButton extends StatelessWidget { + const BaseActionButton({ + super.key, + required this.label, + required this.iconData, + this.iconColor, + this.onPressed, + this.onLongPressed, + this.maxWidth = 90.0, + this.minWidth, + this.menuItem = false, + }); + + final String label; + final IconData iconData; + final Color? iconColor; + final double maxWidth; + final double? minWidth; + final bool menuItem; + final void Function()? onPressed; + final void Function()? onLongPressed; + + @override + Widget build(BuildContext context) { + final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0); + final iconTheme = IconTheme.of(context); + final iconSize = iconTheme.size ?? 24.0; + final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color; + final textColor = context.themeData.textTheme.labelLarge?.color; + + if (menuItem) { + return IconButton( + onPressed: onPressed, + icon: Icon(iconData, size: iconSize, color: iconColor), + ); + } + + return ConstrainedBox( + constraints: BoxConstraints( + maxWidth: maxWidth, + ), + child: MaterialButton( + padding: const EdgeInsets.all(10), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + textColor: textColor, + onPressed: onPressed, + onLongPress: onLongPressed, + minWidth: miniWidth, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(iconData, size: iconSize, color: iconColor), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w400, + ), + maxLines: 3, + textAlign: TextAlign.center, + softWrap: true, + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart new file mode 100644 index 0000000000..c80dbaaf2d --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart @@ -0,0 +1,31 @@ +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/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; + +class CastActionButton extends ConsumerWidget { + const CastActionButton({super.key, this.menuItem = true}); + + final bool menuItem; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); + + return BaseActionButton( + iconData: isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, + iconColor: isCasting ? context.primaryColor : null, // null = default color + label: "cast".t(context: context), + onPressed: () { + showDialog( + context: context, + builder: (context) => const CastDialog(), + ); + }, + menuItem: menuItem, + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart new file mode 100644 index 0000000000..f910a2a9e2 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +/// This delete action has the following behavior: +/// - Set the deletedAt information, put the asset in the trash in the server +/// which will be permanently deleted after the number of days configure by the admin +/// - Prompt to delete the asset locally +class DeleteActionButton extends ConsumerWidget { + final ActionSource source; + final bool showConfirmation; + const DeleteActionButton({super.key, required this.source, this.showConfirmation = false}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + if (showConfirmation) { + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('delete'.t(context: context)), + content: Text('delete_action_confirmation_message'.t(context: context)), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text('cancel'.t(context: context)), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text( + 'confirm'.t(context: context), + style: TextStyle( + color: context.colorScheme.error, + ), + ), + ), + ], + ), + ); + if (confirm != true) return; + } + + final result = await ref.read(actionProvider.notifier).trashRemoteAndDeleteLocal(source); + ref.read(multiSelectProvider.notifier).reset(); + + if (source == ActionSource.viewer) { + EventStream.shared.emit(const ViewerReloadAssetEvent()); + } + + final successMessage = 'delete_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + maxWidth: 110.0, + iconData: Icons.delete_sweep_outlined, + label: "delete".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart new file mode 100644 index 0000000000..7a9465dfb6 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +/// This delete action has the following behavior: +/// - Prompt to delete the asset locally +class DeleteLocalActionButton extends ConsumerWidget { + final ActionSource source; + + const DeleteLocalActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).deleteLocal(source); + ref.read(multiSelectProvider.notifier).reset(); + + if (source == ActionSource.viewer) { + EventStream.shared.emit(const ViewerReloadAssetEvent()); + } + + if (result.count == 0) { + return; + } + + final successMessage = 'delete_local_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + maxWidth: 95.0, + iconData: Icons.no_cell_outlined, + label: "control_bottom_app_bar_delete_from_local".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart new file mode 100644 index 0000000000..4979df904c --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +/// This delete action has the following behavior: +/// - Delete permanently on the server +/// - Prompt to delete the asset locally +class DeletePermanentActionButton extends ConsumerWidget { + final ActionSource source; + + const DeletePermanentActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); + ref.read(multiSelectProvider.notifier).reset(); + + if (source == ActionSource.viewer) { + EventStream.shared.emit(const ViewerReloadAssetEvent()); + } + + final successMessage = 'delete_permanently_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + maxWidth: 110.0, + iconData: Icons.delete_forever, + label: "delete_permanently".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart new file mode 100644 index 0000000000..dafbdbc78e --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +/// This delete action has the following behavior: +/// - Delete permanently on the server +/// - Prompt to delete the asset locally +/// +/// This action is used when the asset is selected in multi-selection mode in the trash page +class DeleteTrashActionButton extends ConsumerWidget { + final ActionSource source; + + const DeleteTrashActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'assets_permanently_deleted_count'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return TextButton.icon( + icon: Icon( + Icons.delete_forever, + color: Colors.red[400], + ), + label: Text( + "delete".t(context: context), + style: TextStyle( + fontSize: 14, + color: Colors.red[400], + fontWeight: FontWeight.bold, + ), + ), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart new file mode 100644 index 0000000000..a6464308e2 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart @@ -0,0 +1,53 @@ +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class DownloadActionButton extends ConsumerWidget { + final ActionSource source; + + const DownloadActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).downloadAll(source); + ref.read(multiSelectProvider.notifier).reset(); + + if (!context.mounted) { + return; + } + + if (!result.success) { + ImmichToast.show( + context: context, + msg: 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.error, + ); + } else if (result.count > 0) { + ImmichToast.show( + context: context, + msg: 'download_action_prompt'.t(context: context, args: {'count': result.count.toString()}), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.success, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.download, + label: "download".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart new file mode 100644 index 0000000000..3db3dde44d --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; + +class EditDateTimeActionButton extends ConsumerWidget { + const EditDateTimeActionButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + maxWidth: 95.0, + iconData: Icons.edit_calendar_outlined, + label: "control_bottom_app_bar_edit_time".t(context: context), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart new file mode 100644 index 0000000000..fc642483be --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class EditLocationActionButton extends ConsumerWidget { + final ActionSource source; + + const EditLocationActionButton({super.key, required this.source}); + + _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).editLocation(source, context); + if (result == null) { + return; + } + + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'edit_location_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.edit_location_alt_outlined, + label: "control_bottom_app_bar_edit_location".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart new file mode 100644 index 0000000000..c330a7bbb1 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class FavoriteActionButton extends ConsumerWidget { + final ActionSource source; + final bool menuItem; + + const FavoriteActionButton({ + super.key, + required this.source, + this.menuItem = false, + }); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).favorite(source); + + if (source == ActionSource.viewer) { + return; + } + + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'favorite_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.favorite_border_rounded, + label: "favorite".t(context: context), + menuItem: menuItem, + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart new file mode 100644 index 0000000000..696b9ff367 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; + +class MotionPhotoActionButton extends ConsumerWidget { + const MotionPhotoActionButton({super.key, this.menuItem = true}); + + final bool menuItem; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isPlaying = ref.watch(isPlayingMotionVideoProvider); + + return BaseActionButton( + iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded, + label: "play_motion_photo".t(context: context), + onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle, + menuItem: menuItem, + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart new file mode 100644 index 0000000000..0fde43b459 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class MoveToLockFolderActionButton extends ConsumerWidget { + final ActionSource source; + + const MoveToLockFolderActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).moveToLockFolder(source); + ref.read(multiSelectProvider.notifier).reset(); + + if (source == ActionSource.viewer) { + EventStream.shared.emit(const ViewerReloadAssetEvent()); + } + + final successMessage = 'move_to_lock_folder_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + maxWidth: 100.0, + iconData: Icons.lock_outline_rounded, + label: "move_to_locked_folder".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart new file mode 100644 index 0000000000..8857a1b2d9 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class RemoveFromAlbumActionButton extends ConsumerWidget { + final String albumId; + final ActionSource source; + + const RemoveFromAlbumActionButton({ + super.key, + required this.albumId, + required this.source, + }); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId); + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'remove_from_album_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.remove_circle_outline, + label: "remove_from_album".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart new file mode 100644 index 0000000000..028abf5596 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class RemoveFromLockFolderActionButton extends ConsumerWidget { + final ActionSource source; + + const RemoveFromLockFolderActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).removeFromLockFolder(source); + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'remove_from_lock_folder_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + maxWidth: 100.0, + iconData: Icons.lock_open_rounded, + label: "remove_from_locked_folder".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart new file mode 100644 index 0000000000..7cdc28e1e8 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class RestoreTrashActionButton extends ConsumerWidget { + final ActionSource source; + + const RestoreTrashActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).restoreTrash(source); + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'assets_restored_count'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return TextButton.icon( + icon: const Icon( + Icons.history_rounded, + ), + label: Text( + 'restore'.t(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart new file mode 100644 index 0000000000..546fbe408d --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart @@ -0,0 +1,55 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class ShareActionButton extends ConsumerWidget { + final ActionSource source; + + const ShareActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).shareAssets(source); + ref.read(multiSelectProvider.notifier).reset(); + + if (!context.mounted) { + return; + } + + if (!result.success) { + ImmichToast.show( + context: context, + msg: 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.error, + ); + } else if (result.count > 0) { + ImmichToast.show( + context: context, + msg: 'share_action_prompt'.t(context: context, args: {'count': result.count.toString()}), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.success, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, + label: 'share'.t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart new file mode 100644 index 0000000000..4a9f6d9bd6 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; + +class ShareLinkActionButton extends ConsumerWidget { + final ActionSource source; + + const ShareLinkActionButton({super.key, required this.source}); + + _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + await ref.read(actionProvider.notifier).shareLink(source, context); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.link_rounded, + label: "share_link".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart new file mode 100644 index 0000000000..d448c5ce86 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class StackActionButton extends ConsumerWidget { + final ActionSource source; + + const StackActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to access stack action'); + } + + final result = await ref.read(actionProvider.notifier).stack(user.id, source); + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'stack_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.filter_none_rounded, + label: "stack".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart new file mode 100644 index 0000000000..d26bdfad04 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +/// This delete action has the following behavior: +/// - Set the deletedAt information, put the asset in the trash in the server +/// which will be permanently deleted after the number of days configure by the admin +class TrashActionButton extends ConsumerWidget { + final ActionSource source; + + const TrashActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).trash(source); + ref.read(multiSelectProvider.notifier).reset(); + + if (source == ActionSource.viewer) { + EventStream.shared.emit(const ViewerReloadAssetEvent()); + } + + final successMessage = 'trash_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + maxWidth: 85.0, + iconData: Icons.delete_outline_rounded, + label: "control_bottom_app_bar_trash_from_immich".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart new file mode 100644 index 0000000000..d01a5cc47b --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class UnArchiveActionButton extends ConsumerWidget { + final ActionSource source; + + const UnArchiveActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).unArchive(source); + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'unarchive_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.unarchive_outlined, + label: "unarchive".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart new file mode 100644 index 0000000000..a45bdfb06a --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class UnFavoriteActionButton extends ConsumerWidget { + final ActionSource source; + final bool menuItem; + + const UnFavoriteActionButton({ + super.key, + required this.source, + this.menuItem = false, + }); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).unFavorite(source); + + if (source == ActionSource.viewer) { + return; + } + + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'unfavorite_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.favorite_rounded, + label: "unfavorite".t(context: context), + onPressed: () => _onTap(context, ref), + menuItem: menuItem, + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart new file mode 100644 index 0000000000..bf96e6ea41 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class UnStackActionButton extends ConsumerWidget { + final ActionSource source; + + const UnStackActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).unStack(source); + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'unstack_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.filter_none_rounded, + label: "unstack".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart new file mode 100644 index 0000000000..9e2fc9b309 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class UploadActionButton extends ConsumerWidget { + final ActionSource source; + + const UploadActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).upload(source); + + final successMessage = 'upload_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + + ref.read(multiSelectProvider.notifier).reset(); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.backup_outlined, + label: "upload".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart new file mode 100644 index 0000000000..5d9378ecaf --- /dev/null +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -0,0 +1,778 @@ +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:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/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/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/remote_album.utils.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_mobile/widgets/common/search_field.dart'; +import 'package:sliver_tools/sliver_tools.dart'; + +typedef AlbumSelectorCallback = void Function(RemoteAlbum album); + +class AlbumSelector extends ConsumerStatefulWidget { + final AlbumSelectorCallback onAlbumSelected; + + const AlbumSelector({ + super.key, + required this.onAlbumSelected, + }); + + @override + ConsumerState createState() => _AlbumSelectorState(); +} + +class _AlbumSelectorState extends ConsumerState { + bool isGrid = false; + final searchController = TextEditingController(); + QuickFilterMode filterMode = QuickFilterMode.all; + final searchFocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + + // Load albums when component mounts + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(remoteAlbumProvider.notifier).refresh(); + }); + + searchController.addListener(() { + onSearch(searchController.text, filterMode); + }); + } + + void onSearch(String searchTerm, QuickFilterMode sortMode) { + final userId = ref.watch(currentUserProvider)?.id; + ref.read(remoteAlbumProvider.notifier).searchAlbums(searchTerm, userId, sortMode); + } + + Future onRefresh() async { + await ref.read(remoteAlbumProvider.notifier).refresh(); + } + + void toggleViewMode() { + setState(() { + isGrid = !isGrid; + }); + } + + void changeFilter(QuickFilterMode sortMode) { + setState(() { + filterMode = sortMode; + }); + } + + void clearSearch() { + setState(() { + filterMode = QuickFilterMode.all; + searchController.clear(); + ref.read(remoteAlbumProvider.notifier).clearSearch(); + }); + } + + @override + void dispose() { + searchController.dispose(); + searchFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final albums = ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums)); + + final userId = ref.watch(currentUserProvider)?.id; + + return MultiSliver( + children: [ + _SearchBar( + searchController: searchController, + searchFocusNode: searchFocusNode, + onSearch: onSearch, + filterMode: filterMode, + onClearSearch: clearSearch, + ), + _QuickFilterButtonRow( + filterMode: filterMode, + onChangeFilter: changeFilter, + onSearch: onSearch, + searchController: searchController, + ), + _QuickSortAndViewMode( + isGrid: isGrid, + onToggleViewMode: toggleViewMode, + ), + isGrid + ? _AlbumGrid( + albums: albums, + userId: userId, + onAlbumSelected: widget.onAlbumSelected, + ) + : _AlbumList( + albums: albums, + userId: userId, + onAlbumSelected: widget.onAlbumSelected, + ), + ], + ); + } +} + +class _SortButton extends ConsumerStatefulWidget { + const _SortButton(); + + @override + ConsumerState<_SortButton> createState() => _SortButtonState(); +} + +class _SortButtonState extends ConsumerState<_SortButton> { + RemoteAlbumSortMode albumSortOption = RemoteAlbumSortMode.lastModified; + bool albumSortIsReverse = true; + + void onMenuTapped(RemoteAlbumSortMode sortMode) { + final selected = albumSortOption == sortMode; + // Switch direction + if (selected) { + setState(() { + albumSortIsReverse = !albumSortIsReverse; + }); + ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums( + sortMode, + isReverse: albumSortIsReverse, + ); + } else { + setState(() { + albumSortOption = sortMode; + }); + ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums( + sortMode, + isReverse: albumSortIsReverse, + ); + } + } + + @override + Widget build(BuildContext context) { + 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: RemoteAlbumSortMode.values + .map( + (sortMode) => MenuItemButton( + leadingIcon: albumSortOption == sortMode + ? albumSortIsReverse + ? Icon( + Icons.keyboard_arrow_down, + color: albumSortOption == sortMode + ? context.colorScheme.onPrimary + : context.colorScheme.onSurface, + ) + : Icon( + Icons.keyboard_arrow_up_rounded, + color: albumSortOption == sortMode + ? context.colorScheme.onPrimary + : context.colorScheme.onSurface, + ) + : const Icon(Icons.abc, color: Colors.transparent), + onPressed: () => onMenuTapped(sortMode), + style: ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.fromLTRB(16, 16, 32, 16), + ), + backgroundColor: WidgetStateProperty.all( + albumSortOption == sortMode ? context.colorScheme.primary : Colors.transparent, + ), + shape: WidgetStateProperty.all( + const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(24), + ), + ), + ), + ), + child: Text( + sortMode.key.t(context: context), + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + color: albumSortOption == sortMode + ? 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: albumSortIsReverse + ? const Icon( + Icons.keyboard_arrow_down, + ) + : const Icon( + Icons.keyboard_arrow_up_rounded, + ), + ), + Text( + albumSortOption.key.t(context: context), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: context.colorScheme.onSurface.withAlpha(225), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _SearchBar extends StatelessWidget { + const _SearchBar({ + required this.searchController, + required this.searchFocusNode, + required this.onSearch, + required this.filterMode, + required this.onClearSearch, + }); + + final TextEditingController searchController; + final FocusNode searchFocusNode; + final void Function(String, QuickFilterMode) onSearch; + final QuickFilterMode filterMode; + final VoidCallback onClearSearch; + + @override + Widget build(BuildContext context) { + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + sliver: SliverToBoxAdapter( + child: 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: onClearSearch, + ) + : null, + controller: searchController, + onChanged: (_) => onSearch(searchController.text, filterMode), + focusNode: searchFocusNode, + onTapOutside: (_) => searchFocusNode.unfocus(), + ), + ), + ), + ); + } +} + +class _QuickFilterButtonRow extends StatelessWidget { + const _QuickFilterButtonRow({ + required this.filterMode, + required this.onChangeFilter, + required this.onSearch, + required this.searchController, + }); + + final QuickFilterMode filterMode; + final void Function(QuickFilterMode) onChangeFilter; + final void Function(String, QuickFilterMode) onSearch; + final TextEditingController searchController; + + @override + Widget build(BuildContext context) { + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: Wrap( + spacing: 4, + runSpacing: 4, + children: [ + _QuickFilterButton( + label: 'all'.tr(), + isSelected: filterMode == QuickFilterMode.all, + onTap: () { + onChangeFilter(QuickFilterMode.all); + onSearch(searchController.text, QuickFilterMode.all); + }, + ), + _QuickFilterButton( + label: 'shared_with_me'.tr(), + isSelected: filterMode == QuickFilterMode.sharedWithMe, + onTap: () { + onChangeFilter(QuickFilterMode.sharedWithMe); + onSearch( + searchController.text, + QuickFilterMode.sharedWithMe, + ); + }, + ), + _QuickFilterButton( + label: 'my_albums'.tr(), + isSelected: filterMode == QuickFilterMode.myAlbums, + onTap: () { + onChangeFilter(QuickFilterMode.myAlbums); + onSearch( + searchController.text, + QuickFilterMode.myAlbums, + ); + }, + ), + ], + ), + ), + ); + } +} + +class _QuickFilterButton extends StatelessWidget { + const _QuickFilterButton({ + 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 _QuickSortAndViewMode extends StatelessWidget { + const _QuickSortAndViewMode({ + required this.isGrid, + required this.onToggleViewMode, + }); + + final bool isGrid; + final VoidCallback onToggleViewMode; + + @override + Widget build(BuildContext context) { + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const _SortButton(), + IconButton( + icon: Icon( + isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, + size: 24, + ), + onPressed: onToggleViewMode, + ), + ], + ), + ), + ); + } +} + +class _AlbumList extends ConsumerWidget { + const _AlbumList({ + required this.albums, + required this.userId, + required this.onAlbumSelected, + }); + + final List albums; + final String? userId; + final AlbumSelectorCallback onAlbumSelected; + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (albums.isEmpty) { + return const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: Text('No albums found'), + ), + ), + ); + } + + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + sliver: SliverList.builder( + itemBuilder: (_, index) { + final album = albums[index]; + + return Padding( + padding: const EdgeInsets.only( + bottom: 8.0, + ), + child: LargeLeadingTile( + title: Text( + album.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + '${'items_count'.t( + context: context, + args: { + 'count': album.assetCount, + }, + )} â€ĸ ${album.ownerId != userId ? 'shared_by_user'.t( + context: context, + args: { + 'user': album.ownerName, + }, + ) : 'owned'.t(context: context)}', + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), + ), + onTap: () => onAlbumSelected(album), + leadingPadding: const EdgeInsets.only( + right: 16, + ), + leading: album.thumbnailAssetId != null + ? ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(15), + ), + child: SizedBox( + width: 80, + height: 80, + child: Thumbnail( + remoteId: album.thumbnailAssetId, + ), + ), + ) + : SizedBox( + width: 80, + height: 80, + child: Container( + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainer, + borderRadius: const BorderRadius.all(Radius.circular(16)), + border: Border.all( + color: context.colorScheme.outline.withAlpha(50), + width: 1, + ), + ), + child: const Icon( + Icons.photo_album_rounded, + size: 24, + color: Colors.grey, + ), + ), + ), + ), + ); + }, + itemCount: albums.length, + ), + ); + } +} + +class _AlbumGrid extends StatelessWidget { + const _AlbumGrid({ + required this.albums, + required this.userId, + required this.onAlbumSelected, + }); + + final List albums; + final String? userId; + final AlbumSelectorCallback onAlbumSelected; + + @override + Widget build(BuildContext context) { + if (albums.isEmpty) { + return const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: Text('No albums found'), + ), + ), + ); + } + + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 250, + mainAxisSpacing: 4, + crossAxisSpacing: 4, + childAspectRatio: .7, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + final album = albums[index]; + return _GridAlbumCard( + album: album, + userId: userId, + onAlbumSelected: onAlbumSelected, + ); + }, + childCount: albums.length, + ), + ), + ); + } +} + +class _GridAlbumCard extends ConsumerWidget { + const _GridAlbumCard({ + required this.album, + required this.userId, + required this.onAlbumSelected, + }); + + final RemoteAlbum album; + final String? userId; + final AlbumSelectorCallback onAlbumSelected; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return GestureDetector( + onTap: () => onAlbumSelected(album), + child: Card( + elevation: 0, + color: context.colorScheme.surfaceBright, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all( + Radius.circular(16), + ), + side: BorderSide( + color: context.colorScheme.onSurface.withAlpha(25), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(15), + ), + child: SizedBox( + width: double.infinity, + child: album.thumbnailAssetId != null + ? Thumbnail( + remoteId: album.thumbnailAssetId, + ) + : Container( + color: context.colorScheme.surfaceContainerHighest, + child: const Icon( + Icons.photo_album_rounded, + size: 40, + color: Colors.grey, + ), + ), + ), + ), + ), + Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + album.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + Text( + '${'items_count'.t( + context: context, + args: { + 'count': album.assetCount, + }, + )} â€ĸ ${album.ownerId != userId ? 'shared_by_user'.t( + context: context, + args: { + 'user': album.ownerName, + }, + ) : 'owned'.t(context: context)}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.labelMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class AddToAlbumHeader extends ConsumerWidget { + const AddToAlbumHeader({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + Future onCreateAlbum() async { + final newAlbum = await ref.read(remoteAlbumProvider.notifier).createAlbum( + title: "Untitled Album", + assetIds: ref.read(multiSelectProvider).selectedAssets.map((e) => (e as RemoteAsset).id).toList(), + ); + + if (newAlbum == null) { + ImmichToast.show( + context: context, + toastType: ToastType.error, + msg: 'errors.failed_to_create_album'.tr(), + ); + return; + } + + context.pushRoute(RemoteAlbumRoute(album: newAlbum)); + } + + return SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + sliver: SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "add_to_album", + style: context.textTheme.titleSmall, + ).tr(), + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), // remove internal padding + minimumSize: const Size(0, 0), // allow shrinking + tapTargetSize: MaterialTapTargetSize.shrinkWrap, // remove extra height + ), + onPressed: onCreateAlbum, + 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/presentation/widgets/asset_viewer/asset_stack.provider.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart new file mode 100644 index 0000000000..e78c8ea8ad --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart @@ -0,0 +1,19 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; + +class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier, BaseAsset?> { + @override + Future> build(BaseAsset? asset) async { + if (asset == null || asset is! RemoteAsset || asset.stackId == null) { + return const []; + } + + return ref.watch(assetServiceProvider).getStack(asset); + } +} + +final stackChildrenNotifier = + AsyncNotifierProvider.autoDispose.family, BaseAsset?>( + StackChildrenNotifier.new, +); diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart new file mode 100644 index 0000000000..92f516157e --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; + +class AssetStackRow extends ConsumerWidget { + const AssetStackRow({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + int opacity = ref.watch( + assetViewerProvider.select((state) => state.backgroundOpacity), + ); + final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); + + if (!showControls) { + opacity = 0; + } + + final asset = ref.watch(assetViewerProvider.select((s) => s.currentAsset)); + + return IgnorePointer( + ignoring: opacity < 255, + child: AnimatedOpacity( + opacity: opacity / 255, + duration: Durations.short2, + child: ref.watch(stackChildrenNotifier(asset)).when( + data: (state) => SizedBox.square( + dimension: 80, + child: _StackList(stack: state), + ), + error: (_, __) => const SizedBox.shrink(), + loading: () => const SizedBox.shrink(), + ), + ), + ); + } +} + +class _StackList extends ConsumerWidget { + final List stack; + + const _StackList({required this.stack}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.only( + left: 5, + right: 5, + bottom: 30, + ), + itemCount: stack.length, + itemBuilder: (ctx, index) { + final asset = stack[index]; + return Padding( + padding: const EdgeInsets.only(right: 5), + child: GestureDetector( + onTap: () { + ref.read(assetViewerProvider.notifier).setStackIndex(index); + ref.read(currentAssetNotifier.notifier).setAsset(asset); + }, + child: Container( + height: 60, + width: 60, + decoration: index == ref.watch(assetViewerProvider.select((s) => s.stackIndex)) + ? const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(6)), + border: Border.fromBorderSide( + BorderSide(color: Colors.white, width: 2), + ), + ) + : const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(6)), + border: null, + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: Stack( + fit: StackFit.expand, + children: [ + Image( + fit: BoxFit.cover, + image: getThumbnailImageProvider( + remoteId: asset.id, + size: const Size.square(60), + ), + ), + if (asset.isVideo) + const Icon( + Icons.play_circle_outline_rounded, + color: Colors.white, + size: 16, + shadows: [ + Shadow( + blurRadius: 5.0, + color: Color.fromRGBO(0, 0, 0, 0.6), + offset: Offset(0.0, 0.0), + ), + ], + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart new file mode 100644 index 0000000000..8fbc28f072 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -0,0 +1,689 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/scroll_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/top_app_bar.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; +import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart'; +import 'package:platform/platform.dart'; + +@RoutePage() +class AssetViewerPage extends StatelessWidget { + final int initialIndex; + final TimelineService timelineService; + final int? heroOffset; + + const AssetViewerPage({ + super.key, + required this.initialIndex, + required this.timelineService, + this.heroOffset, + }); + + @override + Widget build(BuildContext context) { + // This is necessary to ensure that the timeline service is available + // since the Timeline and AssetViewer are on different routes / Widget subtrees. + return ProviderScope( + overrides: [timelineServiceProvider.overrideWithValue(timelineService)], + child: AssetViewer(initialIndex: initialIndex, heroOffset: heroOffset), + ); + } +} + +class AssetViewer extends ConsumerStatefulWidget { + final int initialIndex; + final Platform? platform; + final int? heroOffset; + + const AssetViewer({ + super.key, + required this.initialIndex, + this.platform, + this.heroOffset, + }); + + @override + ConsumerState createState() => _AssetViewerState(); +} + +const double _kBottomSheetMinimumExtent = 0.4; +const double _kBottomSheetSnapExtent = 0.7; + +class _AssetViewerState extends ConsumerState { + late PageController pageController; + late DraggableScrollableController bottomSheetController; + PersistentBottomSheetController? sheetCloseController; + // PhotoViewGallery takes care of disposing it's controllers + PhotoViewControllerBase? viewController; + StreamSubscription? reloadSubscription; + + late Platform platform; + late final int heroOffset; + late PhotoViewControllerValue initialPhotoViewState; + bool? hasDraggedDown; + bool isSnapping = false; + bool blockGestures = false; + bool dragInProgress = false; + bool shouldPopOnDrag = false; + bool assetReloadRequested = false; + double? initialScale; + double previousExtent = _kBottomSheetMinimumExtent; + Offset dragDownPosition = Offset.zero; + int totalAssets = 0; + int stackIndex = 0; + BuildContext? scaffoldContext; + Map videoPlayerKeys = {}; + + // Delayed operations that should be cancelled on disposal + final List _delayedOperations = []; + + @override + void initState() { + super.initState(); + pageController = PageController(initialPage: widget.initialIndex); + platform = widget.platform ?? const LocalPlatform(); + totalAssets = ref.read(timelineServiceProvider).totalAssets; + bottomSheetController = DraggableScrollableController(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _onAssetChanged(widget.initialIndex); + }); + reloadSubscription = EventStream.shared.listen(_onEvent); + heroOffset = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0; + } + + @override + void dispose() { + pageController.dispose(); + bottomSheetController.dispose(); + _cancelTimers(); + reloadSubscription?.cancel(); + super.dispose(); + } + + bool get showingBottomSheet => ref.read(assetViewerProvider.select((s) => s.showingBottomSheet)); + + Color get backgroundColor { + final opacity = ref.read(assetViewerProvider.select((s) => s.backgroundOpacity)); + return Colors.black.withAlpha(opacity); + } + + void _cancelTimers() { + for (final timer in _delayedOperations) { + timer.cancel(); + } + _delayedOperations.clear(); + } + + // This is used to calculate the scale of the asset when the bottom sheet is showing. + // It is a small increment to ensure that the asset is slightly zoomed in when the + // bottom sheet is showing, which emulates the zoom effect. + double get _getScaleForBottomSheet => (viewController?.prevValue.scale ?? viewController?.value.scale ?? 1.0) + 0.01; + + double _getVerticalOffsetForBottomSheet(double extent) => + (context.height * extent) - (context.height * _kBottomSheetMinimumExtent); + + Future _precacheImage(int index) async { + if (!mounted || index < 0 || index >= totalAssets) { + return; + } + + final asset = ref.read(timelineServiceProvider).getAsset(index); + final screenSize = Size(context.width, context.height); + + // Precache both thumbnail and full image for smooth transitions + unawaited( + Future.wait([ + precacheImage( + getThumbnailImageProvider(asset: asset, size: screenSize), + context, + onError: (_, __) {}, + ), + precacheImage( + getFullImageProvider(asset, size: screenSize), + context, + onError: (_, __) {}, + ), + ]), + ); + } + + void _onAssetChanged(int index) { + final asset = ref.read(timelineServiceProvider).getAsset(index); + // Always holds the current asset from the timeline + ref.read(assetViewerProvider.notifier).setAsset(asset); + // The currentAssetNotifier actually holds the current asset that is displayed + // which could be stack children as well + ref.read(currentAssetNotifier.notifier).setAsset(asset); + if (asset.isVideo || asset.isMotionPhoto) { + ref.read(videoPlaybackValueProvider.notifier).reset(); + ref.read(videoPlayerControlsProvider.notifier).pause(); + } + + unawaited(ref.read(timelineServiceProvider).preCacheAssets(index)); + _cancelTimers(); + // This will trigger the pre-caching of adjacent assets ensuring + // that they are ready when the user navigates to them. + final timer = Timer(Durations.medium4, () { + // Check if widget is still mounted before proceeding + if (!mounted) return; + + for (final offset in [-1, 1]) { + unawaited(_precacheImage(index + offset)); + } + }); + _delayedOperations.add(timer); + + _handleCasting(asset); + } + + void _handleCasting(BaseAsset asset) { + if (!ref.read(castProvider).isCasting) return; + + // hide any casting snackbars if they exist + context.scaffoldMessenger.hideCurrentSnackBar(); + + // send image to casting if the server has it + if (asset.hasRemote) { + final remoteAsset = asset as RemoteAsset; + + ref.read(castProvider.notifier).loadMedia(remoteAsset, false); + } else { + // casting cannot show local assets + context.scaffoldMessenger.clearSnackBars(); + + if (ref.read(castProvider).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, + ), + ), + ), + ); + } + } + } + + void _onPageBuild(PhotoViewControllerBase controller) { + viewController ??= controller; + if (showingBottomSheet) { + final verticalOffset = + (context.height * bottomSheetController.size) - (context.height * _kBottomSheetMinimumExtent); + controller.position = Offset(0, -verticalOffset); + } + } + + void _onPageChanged(int index, PhotoViewControllerBase? controller) { + _onAssetChanged(index); + viewController = controller; + + // If the bottom sheet is showing, we need to adjust scale the asset to + // emulate the zoom effect + if (showingBottomSheet) { + initialScale = controller?.scale; + controller?.scale = _getScaleForBottomSheet; + } + } + + void _onDragStart( + _, + DragStartDetails details, + PhotoViewControllerBase controller, + PhotoViewScaleStateController scaleStateController, + ) { + viewController = controller; + dragDownPosition = details.localPosition; + initialPhotoViewState = controller.value; + final isZoomed = scaleStateController.scaleState == PhotoViewScaleState.zoomedIn || + scaleStateController.scaleState == PhotoViewScaleState.covering; + if (!showingBottomSheet && isZoomed) { + blockGestures = true; + } + } + + void _onDragEnd(BuildContext ctx, _, __) { + dragInProgress = false; + + if (shouldPopOnDrag) { + // Dismiss immediately without state updates to avoid rebuilds + ctx.maybePop(); + return; + } + + // Do not reset the state if the bottom sheet is showing + if (showingBottomSheet) { + _snapBottomSheet(); + return; + } + + // If the gestures are blocked, do not reset the state + if (blockGestures) { + blockGestures = false; + return; + } + + shouldPopOnDrag = false; + hasDraggedDown = null; + viewController?.animateMultiple( + position: initialPhotoViewState.position, + scale: initialPhotoViewState.scale, + rotation: initialPhotoViewState.rotation, + ); + ref.read(assetViewerProvider.notifier).setOpacity(255); + } + + void _onDragUpdate(BuildContext ctx, DragUpdateDetails details, _) { + if (blockGestures) { + return; + } + + dragInProgress = true; + final delta = details.localPosition - dragDownPosition; + hasDraggedDown ??= delta.dy > 0; + if (!hasDraggedDown! || showingBottomSheet) { + _handleDragUp(ctx, delta); + return; + } + + _handleDragDown(ctx, delta); + } + + void _handleDragUp(BuildContext ctx, Offset delta) { + const double openThreshold = 50; + + final position = initialPhotoViewState.position + Offset(0, delta.dy); + final distanceToOrigin = position.distance; + + viewController?.updateMultiple(position: position); + // Moves the bottom sheet when the asset is being dragged up + if (showingBottomSheet && bottomSheetController.isAttached) { + final centre = (ctx.height * _kBottomSheetMinimumExtent); + bottomSheetController.jumpTo((centre + distanceToOrigin) / ctx.height); + } + + if (distanceToOrigin > openThreshold && !showingBottomSheet) { + _openBottomSheet(ctx); + } + } + + void _handleDragDown(BuildContext ctx, Offset delta) { + const double dragRatio = 0.2; + const double popThreshold = 75; + + final distance = delta.distance; + shouldPopOnDrag = delta.dy > 0 && distance > popThreshold; + + final maxScaleDistance = ctx.height * 0.5; + final scaleReduction = (distance / maxScaleDistance).clamp(0.0, dragRatio); + double? updatedScale; + if (initialPhotoViewState.scale != null) { + updatedScale = initialPhotoViewState.scale! * (1.0 - scaleReduction); + } + + final backgroundOpacity = (255 * (1.0 - (scaleReduction / dragRatio))).round(); + + viewController?.updateMultiple( + position: initialPhotoViewState.position + delta, + scale: updatedScale, + ); + ref.read(assetViewerProvider.notifier).setOpacity(backgroundOpacity); + } + + void _onTapDown(_, __, ___) { + if (!showingBottomSheet) { + ref.read(assetViewerProvider.notifier).toggleControls(); + } + } + + bool _onNotification(Notification delta) { + if (delta is DraggableScrollableNotification) { + _handleDraggableNotification(delta); + } + + // Handle sheet snap manually so that the it snaps only at _kBottomSheetSnapExtent but not after + // the isSnapping guard is to prevent the notification from recursively handling the + // notification, eventually resulting in a heap overflow + if (!isSnapping && delta is ScrollEndNotification) { + _snapBottomSheet(); + } + return false; + } + + void _handleDraggableNotification(DraggableScrollableNotification delta) { + final currentExtent = delta.extent; + final isDraggingDown = currentExtent < previousExtent; + previousExtent = currentExtent; + // Closes the bottom sheet if the user is dragging down + if (isDraggingDown && delta.extent < 0.55) { + if (dragInProgress) { + blockGestures = true; + } + sheetCloseController?.close(); + } + + // If the asset is being dragged down, we do not want to update the asset position again + if (dragInProgress) { + return; + } + + final verticalOffset = _getVerticalOffsetForBottomSheet(delta.extent); + // Moves the asset when the bottom sheet is being dragged + if (verticalOffset > 0) { + viewController?.position = Offset(0, -verticalOffset); + } + } + + void _onEvent(Event event) { + if (event is TimelineReloadEvent) { + _onTimelineReloadEvent(); + return; + } + + if (event is ViewerReloadAssetEvent) { + assetReloadRequested = true; + return; + } + + if (event is ViewerOpenBottomSheetEvent) { + final extent = _kBottomSheetMinimumExtent + 0.3; + _openBottomSheet(scaffoldContext!, extent: extent); + final offset = _getVerticalOffsetForBottomSheet(extent); + viewController?.position = Offset(0, -offset); + return; + } + } + + void _onTimelineReloadEvent() { + totalAssets = ref.read(timelineServiceProvider).totalAssets; + if (totalAssets == 0) { + context.maybePop(); + return; + } + + if (assetReloadRequested) { + assetReloadRequested = false; + _onAssetReloadEvent(); + return; + } + } + + void _onAssetReloadEvent() { + setState(() { + final index = pageController.page?.round() ?? 0; + final newAsset = ref.read(timelineServiceProvider).getAsset(index); + final currentAsset = ref.read(currentAssetNotifier); + // Do not reload / close the bottom sheet if the asset has not changed + if (newAsset.heroTag == currentAsset?.heroTag) { + return; + } + + _onAssetChanged(pageController.page!.round()); + sheetCloseController?.close(); + }); + } + + void _openBottomSheet( + BuildContext ctx, { + double extent = _kBottomSheetMinimumExtent, + }) { + ref.read(assetViewerProvider.notifier).setBottomSheet(true); + initialScale = viewController?.scale; + viewController?.updateMultiple(scale: _getScaleForBottomSheet); + previousExtent = _kBottomSheetMinimumExtent; + sheetCloseController = showBottomSheet( + context: ctx, + sheetAnimationStyle: const AnimationStyle( + duration: Durations.short4, + reverseDuration: Durations.short2, + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)), + ), + backgroundColor: ctx.colorScheme.surfaceContainerLowest, + builder: (_) { + return NotificationListener( + onNotification: _onNotification, + child: AssetDetailBottomSheet( + controller: bottomSheetController, + initialChildSize: extent, + ), + ); + }, + ); + sheetCloseController?.closed.then((_) => _handleSheetClose()); + } + + void _handleSheetClose() { + viewController?.animateMultiple(position: Offset.zero); + viewController?.updateMultiple(scale: initialScale); + ref.read(assetViewerProvider.notifier).setBottomSheet(false); + sheetCloseController = null; + shouldPopOnDrag = false; + hasDraggedDown = null; + } + + void _snapBottomSheet() { + if (bottomSheetController.size > _kBottomSheetSnapExtent || bottomSheetController.size < 0.4) { + return; + } + isSnapping = true; + bottomSheetController.animateTo( + _kBottomSheetSnapExtent, + duration: Durations.short3, + curve: Curves.easeOut, + ); + } + + Widget _placeholderBuilder( + BuildContext ctx, + ImageChunkEvent? progress, + int index, + ) { + BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index); + final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull; + if (stackChildren != null && stackChildren.isNotEmpty) { + asset = stackChildren.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex))); + } + return Container( + width: double.infinity, + height: double.infinity, + color: backgroundColor, + child: Thumbnail( + asset: asset, + fit: BoxFit.contain, + size: Size( + ctx.width, + ctx.height, + ), + ), + ); + } + + void _onScaleStateChanged(PhotoViewScaleState scaleState) { + if (scaleState != PhotoViewScaleState.initial) { + ref.read(videoPlayerControlsProvider.notifier).pause(); + } + } + + void _onLongPress(_, __, ___) { + ref.read(isPlayingMotionVideoProvider.notifier).playing = true; + } + + PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) { + scaffoldContext ??= ctx; + BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index); + final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull; + if (stackChildren != null && stackChildren.isNotEmpty) { + asset = stackChildren.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex))); + } + + final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider); + if (asset.isImage && !isPlayingMotionVideo) { + return _imageBuilder(ctx, asset); + } + + return _videoBuilder(ctx, asset); + } + + PhotoViewGalleryPageOptions _imageBuilder(BuildContext ctx, BaseAsset asset) { + final size = Size(ctx.width, ctx.height); + return PhotoViewGalleryPageOptions( + key: ValueKey(asset.heroTag), + imageProvider: getFullImageProvider(asset, size: size), + heroAttributes: PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'), + filterQuality: FilterQuality.high, + tightMode: true, + initialScale: PhotoViewComputedScale.contained * 0.999, + minScale: PhotoViewComputedScale.contained * 0.999, + disableScaleGestures: showingBottomSheet, + onDragStart: _onDragStart, + onDragUpdate: _onDragUpdate, + onDragEnd: _onDragEnd, + onTapDown: _onTapDown, + onLongPressStart: asset.isMotionPhoto ? _onLongPress : null, + errorBuilder: (_, __, ___) => Container( + width: ctx.width, + height: ctx.height, + color: backgroundColor, + child: Thumbnail( + asset: asset, + fit: BoxFit.contain, + size: size, + ), + ), + ); + } + + GlobalKey _getVideoPlayerKey(String id) { + videoPlayerKeys.putIfAbsent(id, () => GlobalKey()); + return videoPlayerKeys[id]!; + } + + PhotoViewGalleryPageOptions _videoBuilder(BuildContext ctx, BaseAsset asset) { + return PhotoViewGalleryPageOptions.customChild( + onDragStart: _onDragStart, + onDragUpdate: _onDragUpdate, + onDragEnd: _onDragEnd, + onTapDown: _onTapDown, + heroAttributes: PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'), + filterQuality: FilterQuality.high, + initialScale: PhotoViewComputedScale.contained * 0.99, + maxScale: 1.0, + minScale: PhotoViewComputedScale.contained * 0.99, + basePosition: Alignment.center, + child: SizedBox( + width: ctx.width, + height: ctx.height, + child: NativeVideoViewer( + key: _getVideoPlayerKey(asset.heroTag), + asset: asset, + image: Image( + key: ValueKey(asset), + image: getFullImageProvider(asset, size: Size(ctx.width, ctx.height)), + fit: BoxFit.contain, + height: ctx.height, + width: ctx.width, + alignment: Alignment.center, + ), + ), + ), + ); + } + + void _onPop(bool didPop, T? result) { + ref.read(currentAssetNotifier.notifier).dispose(); + } + + @override + Widget build(BuildContext context) { + // Rebuild the widget when the asset viewer state changes + // Using multiple selectors to avoid unnecessary rebuilds for other state changes + ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet)); + ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)); + ref.watch(assetViewerProvider.select((s) => s.stackIndex)); + ref.watch(isPlayingMotionVideoProvider); + + // Listen for casting changes and send initial asset to the cast provider + ref.listen(castProvider.select((value) => value.isCasting), (_, isCasting) async { + if (!isCasting) return; + + final asset = ref.read(currentAssetNotifier); + if (asset == null) return; + + WidgetsBinding.instance.addPostFrameCallback((_) { + _handleCasting(asset); + }); + }); + + final isInLockedView = ref.watch(inLockedViewProvider); + + // Currently it is not possible to scroll the asset when the bottom sheet is open all the way. + // Issue: https://github.com/flutter/flutter/issues/109037 + // TODO: Add a custom scrum builder once the fix lands on stable + return PopScope( + onPopInvokedWithResult: _onPop, + child: Scaffold( + backgroundColor: backgroundColor, + appBar: const ViewerTopAppBar(), + extendBody: true, + extendBodyBehindAppBar: true, + body: PhotoViewGallery.builder( + gaplessPlayback: true, + loadingBuilder: _placeholderBuilder, + pageController: pageController, + scrollPhysics: platform.isIOS + ? const FastScrollPhysics() // Use bouncing physics for iOS + : const FastClampingScrollPhysics() // Use heavy physics for Android + , + itemCount: totalAssets, + onPageChanged: _onPageChanged, + onPageBuild: _onPageBuild, + scaleStateChangedCallback: _onScaleStateChanged, + builder: _assetBuilder, + backgroundDecoration: BoxDecoration(color: backgroundColor), + enablePanAlways: true, + ), + bottomNavigationBar: showingBottomSheet + ? const SizedBox.shrink() + : Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const AssetStackRow(), + if (!isInLockedView) const ViewerBottomBar(), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart new file mode 100644 index 0000000000..32d5249bdc --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart @@ -0,0 +1,113 @@ +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +class ViewerOpenBottomSheetEvent extends Event { + const ViewerOpenBottomSheetEvent(); +} + +class ViewerReloadAssetEvent extends Event { + const ViewerReloadAssetEvent(); +} + +class AssetViewerState { + final int backgroundOpacity; + final bool showingBottomSheet; + final bool showingControls; + final BaseAsset? currentAsset; + final int stackIndex; + + const AssetViewerState({ + this.backgroundOpacity = 255, + this.showingBottomSheet = false, + this.showingControls = true, + this.currentAsset, + this.stackIndex = 0, + }); + + AssetViewerState copyWith({ + int? backgroundOpacity, + bool? showingBottomSheet, + bool? showingControls, + BaseAsset? currentAsset, + int? stackIndex, + }) { + return AssetViewerState( + backgroundOpacity: backgroundOpacity ?? this.backgroundOpacity, + showingBottomSheet: showingBottomSheet ?? this.showingBottomSheet, + showingControls: showingControls ?? this.showingControls, + currentAsset: currentAsset ?? this.currentAsset, + stackIndex: stackIndex ?? this.stackIndex, + ); + } + + @override + String toString() { + return 'AssetViewerState(opacity: $backgroundOpacity, bottomSheet: $showingBottomSheet, controls: $showingControls)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is AssetViewerState && + other.backgroundOpacity == backgroundOpacity && + other.showingBottomSheet == showingBottomSheet && + other.showingControls == showingControls && + other.currentAsset == currentAsset && + other.stackIndex == stackIndex; + } + + @override + int get hashCode => + backgroundOpacity.hashCode ^ + showingBottomSheet.hashCode ^ + showingControls.hashCode ^ + currentAsset.hashCode ^ + stackIndex.hashCode; +} + +class AssetViewerStateNotifier extends AutoDisposeNotifier { + @override + AssetViewerState build() { + return const AssetViewerState(); + } + + void setAsset(BaseAsset? asset) { + state = state.copyWith(currentAsset: asset, stackIndex: 0); + } + + void setOpacity(int opacity) { + state = state.copyWith( + backgroundOpacity: opacity, + showingControls: opacity == 255 ? true : state.showingControls, + ); + } + + void setBottomSheet(bool showing) { + state = state.copyWith( + showingBottomSheet: showing, + showingControls: showing ? true : state.showingControls, + ); + if (showing) { + ref.read(videoPlayerControlsProvider.notifier).pause(); + } + } + + void setControls(bool isShowing) { + state = state.copyWith(showingControls: isShowing); + } + + void toggleControls() { + state = state.copyWith(showingControls: !state.showingControls); + } + + void setStackIndex(int index) { + state = state.copyWith(stackIndex: index); + } +} + +final assetViewerProvider = AutoDisposeNotifierProvider( + AssetViewerStateNotifier.new, +); diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart new file mode 100644 index 0000000000..8c04fd5a85 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -0,0 +1,92 @@ +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/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +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/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; + +class ViewerBottomBar extends ConsumerWidget { + const ViewerBottomBar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetNotifier); + if (asset == null) { + return const SizedBox.shrink(); + } + + final user = ref.watch(currentUserProvider); + final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; + final isSheetOpen = ref.watch( + assetViewerProvider.select((s) => s.showingBottomSheet), + ); + int opacity = ref.watch( + assetViewerProvider.select((state) => state.backgroundOpacity), + ); + final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); + + if (!showControls) { + opacity = 0; + } + + final actions = [ + const ShareActionButton(source: ActionSource.viewer), + if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), + if (asset.hasRemote && isOwner) const ArchiveActionButton(source: ActionSource.viewer), + asset.isLocalOnly + ? const DeleteLocalActionButton( + source: ActionSource.viewer, + ) + : const DeleteActionButton( + source: ActionSource.viewer, + showConfirmation: true, + ), + ]; + + return IgnorePointer( + ignoring: opacity < 255, + child: AnimatedOpacity( + opacity: opacity / 255, + duration: Durations.short2, + child: AnimatedSwitcher( + duration: Durations.short4, + child: isSheetOpen + ? const SizedBox.shrink() + : Theme( + data: context.themeData.copyWith( + iconTheme: const IconThemeData(size: 22, color: Colors.white), + textTheme: context.themeData.textTheme.copyWith( + labelLarge: context.themeData.textTheme.labelLarge?.copyWith( + color: Colors.white, + ), + ), + ), + child: Container( + height: context.padding.bottom + (asset.isVideo ? 160 : 90), + color: Colors.black.withAlpha(125), + padding: EdgeInsets.only(bottom: context.padding.bottom), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (asset.isVideo) const VideoControls(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: actions, + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart new file mode 100644 index 0000000000..e24bb3d7c0 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -0,0 +1,256 @@ +import 'package:easy_localization/easy_localization.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/exif.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/action_buttons/archive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_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/share_link_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/utils/bytes_units.dart'; + +const _kSeparator = ' â€ĸ '; + +class AssetDetailBottomSheet extends ConsumerWidget { + final DraggableScrollableController? controller; + final double initialChildSize; + + const AssetDetailBottomSheet({ + this.controller, + this.initialChildSize = 0.35, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetNotifier); + if (asset == null) { + return const SizedBox.shrink(); + } + + final isTrashEnable = ref.watch( + serverInfoProvider.select((state) => state.serverFeatures.trash), + ); + + final isInLockedView = ref.watch(inLockedViewProvider); + + final actions = [ + const ShareActionButton(source: ActionSource.viewer), + if (asset.hasRemote) ...[ + const ShareLinkActionButton(source: ActionSource.viewer), + const ArchiveActionButton(source: ActionSource.viewer), + if (!asset.hasLocal) const DownloadActionButton(source: ActionSource.viewer), + isTrashEnable + ? const TrashActionButton(source: ActionSource.viewer) + : const DeletePermanentActionButton(source: ActionSource.viewer), + const DeleteActionButton(source: ActionSource.viewer), + const MoveToLockFolderActionButton( + source: ActionSource.viewer, + ), + ], + if (asset.storage == AssetState.local) ...[ + const DeleteLocalActionButton(source: ActionSource.viewer), + const UploadActionButton(source: ActionSource.timeline), + ], + ]; + + final lockedViewActions = []; + + return BaseBottomSheet( + actions: isInLockedView ? lockedViewActions : actions, + slivers: const [_AssetDetailBottomSheet()], + controller: controller, + initialChildSize: initialChildSize, + minChildSize: 0.1, + maxChildSize: 0.88, + expand: false, + shouldCloseOnMinExtent: false, + resizeOnScroll: false, + backgroundColor: context.isDarkTheme ? Colors.black : Colors.white, + ); + } +} + +class _AssetDetailBottomSheet extends ConsumerWidget { + const _AssetDetailBottomSheet(); + + String _getDateTime(BuildContext ctx, BaseAsset asset) { + final dateTime = asset.createdAt.toLocal(); + final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime); + final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime); + final timezone = dateTime.timeZoneOffset.isNegative + ? 'UTC-${dateTime.timeZoneOffset.inHours.abs().toString().padLeft(2, '0')}:${(dateTime.timeZoneOffset.inMinutes.abs() % 60).toString().padLeft(2, '0')}' + : 'UTC+${dateTime.timeZoneOffset.inHours.toString().padLeft(2, '0')}:${(dateTime.timeZoneOffset.inMinutes.abs() % 60).toString().padLeft(2, '0')}'; + return '$date$_kSeparator$time $timezone'; + } + + String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) { + final height = asset.height ?? exifInfo?.height; + final width = asset.width ?? exifInfo?.width; + final resolution = (width != null && height != null) ? "${width.toInt()} x ${height.toInt()}" : null; + final fileSize = exifInfo?.fileSize != null ? formatBytes(exifInfo!.fileSize!) : null; + + return switch ((fileSize, resolution)) { + (null, null) => '', + (String fileSize, null) => fileSize, + (null, String resolution) => resolution, + (String fileSize, String resolution) => '$fileSize$_kSeparator$resolution', + }; + } + + String? _getCameraInfoTitle(ExifInfo? exifInfo) { + if (exifInfo == null) { + return null; + } + + return switch ((exifInfo.make, exifInfo.model)) { + (null, null) => null, + (String make, null) => make, + (null, String model) => model, + (String make, String model) => '$make $model', + }; + } + + String? _getCameraInfoSubtitle(ExifInfo? exifInfo) { + if (exifInfo == null) { + return null; + } + + final fNumber = exifInfo.fNumber.isNotEmpty ? 'ƒ/${exifInfo.fNumber}' : null; + final exposureTime = exifInfo.exposureTime.isNotEmpty ? exifInfo.exposureTime : null; + final focalLength = exifInfo.focalLength.isNotEmpty ? '${exifInfo.focalLength} mm' : null; + final iso = exifInfo.iso != null ? 'ISO ${exifInfo.iso}' : null; + + return [fNumber, exposureTime, focalLength, iso].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetNotifier); + if (asset == null) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + + final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull; + final cameraTitle = _getCameraInfoTitle(exifInfo); + + return SliverList.list( + children: [ + // Asset Date and Time + _SheetTile( + title: _getDateTime(context, asset), + titleStyle: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SheetLocationDetails(), + // Details header + _SheetTile( + title: 'exif_bottom_sheet_details'.t(context: context), + titleStyle: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ), + // File info + _SheetTile( + title: asset.name, + titleStyle: context.textTheme.labelLarge, + leading: Icon( + asset.isImage ? Icons.image_outlined : Icons.videocam_outlined, + size: 24, + color: context.textTheme.labelLarge?.color, + ), + subtitle: _getFileInfo(asset, exifInfo), + subtitleStyle: context.textTheme.bodyMedium?.copyWith( + color: context.textTheme.bodyMedium?.color?.withAlpha(155), + ), + ), + // Camera info + if (cameraTitle != null) + _SheetTile( + title: cameraTitle, + titleStyle: context.textTheme.labelLarge, + leading: Icon( + Icons.camera_outlined, + size: 24, + color: context.textTheme.labelLarge?.color, + ), + subtitle: _getCameraInfoSubtitle(exifInfo), + subtitleStyle: context.textTheme.bodyMedium?.copyWith( + color: context.textTheme.bodyMedium?.color?.withAlpha(155), + ), + ), + ], + ); + } +} + +class _SheetTile extends StatelessWidget { + final String title; + final Widget? leading; + final String? subtitle; + final TextStyle? titleStyle; + final TextStyle? subtitleStyle; + + const _SheetTile({ + required this.title, + this.titleStyle, + this.leading, + this.subtitle, + this.subtitleStyle, + }); + + @override + Widget build(BuildContext context) { + final Widget titleWidget; + if (leading == null) { + titleWidget = LimitedBox( + maxWidth: double.infinity, + child: Text(title, style: titleStyle), + ); + } else { + titleWidget = Container( + width: double.infinity, + padding: const EdgeInsets.only(left: 15), + child: Text(title, style: titleStyle), + ); + } + + final Widget? subtitleWidget; + if (leading == null && subtitle != null) { + subtitleWidget = Text(subtitle!, style: subtitleStyle); + } else if (leading != null && subtitle != null) { + subtitleWidget = Padding( + padding: const EdgeInsets.only(left: 15), + child: Text(subtitle!, style: subtitleStyle), + ); + } else { + subtitleWidget = null; + } + + return ListTile( + dense: true, + visualDensity: VisualDensity.compact, + title: titleWidget, + titleAlignment: ListTileTitleAlignment.center, + leading: leading, + contentPadding: leading == null ? null : const EdgeInsets.only(left: 25), + subtitle: subtitleWidget, + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart new file mode 100644 index 0000000000..f91dafb3ed --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; + +class SheetLocationDetails extends ConsumerStatefulWidget { + const SheetLocationDetails({super.key}); + + @override + ConsumerState createState() => _SheetLocationDetailsState(); +} + +class _SheetLocationDetailsState extends ConsumerState { + BaseAsset? asset; + ExifInfo? exifInfo; + MapLibreMapController? _mapController; + + String? _getLocationName(ExifInfo? exifInfo) { + if (exifInfo == null) { + return null; + } + + final cityName = exifInfo.city; + final stateName = exifInfo.state; + + if (cityName != null && stateName != null) { + return "$cityName, $stateName"; + } + return null; + } + + void _onMapCreated(MapLibreMapController controller) { + _mapController = controller; + } + + void _onExifChanged( + AsyncValue? previous, + AsyncValue current, + ) { + asset = ref.read(currentAssetNotifier); + setState(() { + exifInfo = current.valueOrNull; + final hasCoordinates = exifInfo?.hasCoordinates ?? false; + if (exifInfo != null && hasCoordinates) { + _mapController?.moveCamera( + CameraUpdate.newLatLng( + LatLng(exifInfo!.latitude!, exifInfo!.longitude!), + ), + ); + } + }); + } + + @override + void initState() { + super.initState(); + ref.listenManual( + currentAssetExifProvider, + _onExifChanged, + fireImmediately: true, + ); + } + + @override + Widget build(BuildContext context) { + final hasCoordinates = exifInfo?.hasCoordinates ?? false; + + // Guard no lat/lng + if (!hasCoordinates || (asset != null && asset is LocalAsset && asset!.hasRemote)) { + return const SizedBox.shrink(); + } + + final remoteId = asset is LocalAsset ? (asset as LocalAsset).remoteId : (asset as RemoteAsset).id; + final locationName = _getLocationName(exifInfo); + final coordinates = "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}"; + + return Padding( + padding: EdgeInsets.symmetric( + vertical: 16.0, + horizontal: context.isMobile ? 16.0 : 56.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + "exif_bottom_sheet_location".t(context: context), + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ), + ), + ExifMap( + exifInfo: exifInfo!, + markerId: remoteId, + onMapCreated: _onMapCreated, + ), + const SizedBox(height: 15), + if (locationName != null) + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + locationName, + style: context.textTheme.labelLarge, + ), + ), + Text( + coordinates, + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(150), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart new file mode 100644 index 0000000000..3f48a83bcb --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart @@ -0,0 +1,136 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/providers/websocket.provider.dart'; + +class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { + const ViewerTopAppBar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetNotifier); + if (asset == null) { + return const SizedBox.shrink(); + } + + final user = ref.watch(currentUserProvider); + final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; + final isInLockedView = ref.watch(inLockedViewProvider); + + final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet)); + int opacity = ref.watch( + assetViewerProvider.select((state) => state.backgroundOpacity), + ); + final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); + + if (!showControls) { + opacity = 0; + } + + final isCasting = ref.watch( + castProvider.select((c) => c.isCasting), + ); + final websocketConnected = ref.watch(websocketProvider.select((c) => c.isConnected)); + + final actions = [ + if (isCasting || (asset.hasRemote && websocketConnected)) + const CastActionButton( + menuItem: true, + ), + if (asset.hasRemote && isOwner && !asset.isFavorite) + const FavoriteActionButton(source: ActionSource.viewer, menuItem: true), + if (asset.hasRemote && isOwner && asset.isFavorite) + const UnFavoriteActionButton( + source: ActionSource.viewer, + menuItem: true, + ), + if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true), + const _KebabMenu(), + ]; + + final lockedViewActions = [ + if (isCasting || (asset.hasRemote && websocketConnected)) + const CastActionButton( + menuItem: true, + ), + const _KebabMenu(), + ]; + + return IgnorePointer( + ignoring: opacity < 255, + child: AnimatedOpacity( + opacity: opacity / 255, + duration: Durations.short2, + child: AppBar( + backgroundColor: isShowingSheet ? Colors.transparent : Colors.black.withAlpha(125), + leading: const _AppBarBackButton(), + iconTheme: const IconThemeData(size: 22, color: Colors.white), + actionsIconTheme: const IconThemeData(size: 22, color: Colors.white), + shape: const Border(), + actions: isShowingSheet + ? null + : isInLockedView + ? lockedViewActions + : actions, + ), + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(60.0); +} + +class _KebabMenu extends ConsumerWidget { + const _KebabMenu(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return IconButton( + onPressed: () { + EventStream.shared.emit(const ViewerOpenBottomSheetEvent()); + }, + icon: const Icon(Icons.more_vert_rounded), + ); + } +} + +class _AppBarBackButton extends ConsumerWidget { + const _AppBarBackButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet)); + final backgroundColor = isShowingSheet && !context.isDarkTheme ? Colors.white : Colors.black; + final foregroundColor = isShowingSheet && !context.isDarkTheme ? Colors.black : Colors.white; + + return Padding( + padding: const EdgeInsets.only(left: 12.0), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: backgroundColor, + shape: const CircleBorder(), + iconSize: 22, + iconColor: foregroundColor, + padding: EdgeInsets.zero, + elevation: isShowingSheet ? 4 : 0, + ), + onPressed: context.maybePop, + child: const Icon(Icons.arrow_back_rounded), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart new file mode 100644 index 0000000000..f0d665b8ce --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -0,0 +1,432 @@ +import 'dart:async'; +import 'dart:io'; + +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/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/setting.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/debounce.dart'; +import 'package:immich_mobile/utils/hooks/interval_hook.dart'; +import 'package:logging/logging.dart'; +import 'package:native_video_player/native_video_player.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +bool _isCurrentAsset( + BaseAsset asset, + BaseAsset? currentAsset, +) { + if (asset is RemoteAsset) { + return switch (currentAsset) { + RemoteAsset remoteAsset => remoteAsset.id == asset.id, + LocalAsset localAsset => localAsset.remoteId == asset.id, + _ => false, + }; + } else if (asset is LocalAsset) { + return switch (currentAsset) { + RemoteAsset remoteAsset => remoteAsset.localId == asset.id, + LocalAsset localAsset => localAsset.id == asset.id, + _ => false, + }; + } + return false; +} + +class NativeVideoViewer extends HookConsumerWidget { + final BaseAsset asset; + final bool showControls; + final int playbackDelayFactor; + final Widget image; + + const NativeVideoViewer({ + super.key, + required this.asset, + required this.image, + this.showControls = true, + this.playbackDelayFactor = 1, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controller = useState(null); + final lastVideoPosition = useRef(-1); + final isBuffering = useRef(false); + + // Used to track whether the video should play when the app + // is brought back to the foreground + final shouldPlayOnForeground = useRef(true); + + // When a video is opened through the timeline, `isCurrent` will immediately be true. + // When swiping from video A to video B, `isCurrent` will initially be true for video A and false for video B. + // If the swipe is completed, `isCurrent` will be true for video B after a delay. + // If the swipe is canceled, `currentAsset` will not have changed and video A will continue to play. + final currentAsset = useState(ref.read(currentAssetNotifier)); + final isCurrent = _isCurrentAsset(asset, currentAsset.value); + + // Used to show the placeholder during hero animations for remote videos to avoid a stutter + final isVisible = useState(Platform.isIOS && asset.hasLocal); + + final log = Logger('NativeVideoViewerPage'); + + final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); + + Future createSource() async { + if (!context.mounted) { + return null; + } + + try { + if (asset.hasLocal && asset.livePhotoVideoId == null) { + final id = asset is LocalAsset ? (asset as LocalAsset).id : (asset as RemoteAsset).localId!; + final file = await const StorageRepository().getFileForAsset(id); + if (file == null) { + throw Exception('No file found for the video'); + } + + final source = await VideoSource.init( + path: file.path, + type: VideoSourceType.file, + ); + return source; + } + + final remoteId = (asset as RemoteAsset).id; + + // Use a network URL for the video player controller + final serverEndpoint = Store.get(StoreKey.serverEndpoint); + final isOriginalVideo = ref.read(settingsProvider).get(Setting.loadOriginalVideo); + final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; + final String videoUrl = asset.livePhotoVideoId != null + ? '$serverEndpoint/assets/${asset.livePhotoVideoId}/$postfixUrl' + : '$serverEndpoint/assets/$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.name}: $error', + ); + return null; + } + } + + final videoSource = useMemoized>(() => createSource()); + final aspectRatio = useState(null); + 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.name}: $error', + ); + } + }, + [asset.heroTag], + ); + + void checkIfBuffering() { + if (!context.mounted) { + return; + } + + final videoPlayback = ref.read(videoPlaybackValueProvider); + if ((isBuffering.value || videoPlayback.state == VideoPlaybackState.initializing) && + videoPlayback.state != VideoPlaybackState.buffering) { + ref.read(videoPlaybackValueProvider.notifier).value = + videoPlayback.copyWith(state: VideoPlaybackState.buffering); + } + } + + // Timer to mark videos as buffering if the position does not change + useInterval(const Duration(seconds: 5), checkIfBuffering); + + // When the position changes, seek to the position + // Debounce the seek to avoid seeking too often + // But also don't delay the seek too much to maintain visual feedback + final seekDebouncer = useDebouncer( + interval: const Duration(milliseconds: 100), + maxWaitTime: const Duration(milliseconds: 200), + ); + ref.listen(videoPlayerControlsProvider, (oldControls, newControls) async { + final playerController = controller.value; + if (playerController == null) { + return; + } + + final playbackInfo = playerController.playbackInfo; + if (playbackInfo == null) { + return; + } + + final oldSeek = (oldControls?.position ?? 0) ~/ 1; + final newSeek = newControls.position ~/ 1; + if (oldSeek != newSeek || newControls.restarted) { + seekDebouncer.run(() => playerController.seekTo(newSeek)); + } + + if (oldControls?.pause != newControls.pause || newControls.restarted) { + // Make sure the last seek is complete before pausing or playing + // Otherwise, `onPlaybackPositionChanged` can receive outdated events + if (seekDebouncer.isActive) { + await seekDebouncer.drain(); + } + + try { + if (newControls.pause) { + await playerController.pause(); + } else { + await playerController.play(); + } + } catch (error) { + log.severe('Error pausing or playing video: $error'); + } + } + }); + + void onPlaybackReady() async { + final videoController = controller.value; + if (videoController == null || !isCurrent || !context.mounted) { + return; + } + + final videoPlayback = VideoPlaybackValue.fromNativeController(videoController); + ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; + + if (ref.read(assetViewerProvider.select((s) => s.showingBottomSheet))) { + return; + } + + try { + await videoController.play(); + await videoController.setVolume(0.9); + } catch (error) { + log.severe('Error playing video: $error'); + } + } + + void onPlaybackStatusChanged() { + final videoController = controller.value; + if (videoController == null || !context.mounted) { + return; + } + + final videoPlayback = VideoPlaybackValue.fromNativeController(videoController); + if (videoPlayback.state == VideoPlaybackState.playing) { + // Sync with the controls playing + WakelockPlus.enable(); + } else { + // Sync with the controls pause + WakelockPlus.disable(); + } + + ref.read(videoPlaybackValueProvider.notifier).status = videoPlayback.state; + } + + void onPlaybackPositionChanged() { + // When seeking, these events sometimes move the slider to an older position + if (seekDebouncer.isActive) { + return; + } + + final videoController = controller.value; + if (videoController == null || !context.mounted) { + return; + } + + final playbackInfo = videoController.playbackInfo; + if (playbackInfo == null) { + return; + } + + ref.read(videoPlaybackValueProvider.notifier).position = Duration(seconds: playbackInfo.position); + + // Check if the video is buffering + if (playbackInfo.status == PlaybackStatus.playing) { + isBuffering.value = lastVideoPosition.value == playbackInfo.position; + lastVideoPosition.value = playbackInfo.position; + } else { + isBuffering.value = false; + lastVideoPosition.value = -1; + } + } + + void onPlaybackEnded() { + final videoController = controller.value; + if (videoController == null || !context.mounted) { + return; + } + + if (videoController.playbackInfo?.status == PlaybackStatus.stopped) { + 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; + } + ref.read(videoPlayerControlsProvider.notifier).reset(); + ref.read(videoPlaybackValueProvider.notifier).reset(); + + final source = await videoSource; + if (source == null) { + return; + } + + nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged); + nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged); + nc.onPlaybackReady.addListener(onPlaybackReady); + nc.onPlaybackEnded.addListener(onPlaybackEnded); + + nc.loadVideoSource(source).catchError((error) { + log.severe('Error loading video source: $error'); + }); + final loopVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo); + nc.setLoop(!asset.isMotionPhoto && loopVideo); + + controller.value = nc; + Timer(const Duration(milliseconds: 200), checkIfBuffering); + } + + ref.listen(currentAssetNotifier, (_, 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 + // Note, in some circumstances a longer delay is needed (eg: memories), + // the playbackDelayFactor can be used for this + // This delay seems like a hacky way to resolve underlying bugs in video + // playback, but other resolutions failed thus far + 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'); + }); + + WakelockPlus.disable(); + }; + }, + const [], + ); + + useOnAppLifecycleStateChange((_, state) async { + if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) { + controller.value?.play(); + } else if (state == AppLifecycleState.paused) { + final videoPlaying = await controller.value?.isPlaying(); + if (videoPlaying ?? true) { + shouldPlayOnForeground.value = true; + controller.value?.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 + Center(key: ValueKey(asset.heroTag), 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) const Center(child: VideoViewerControls()), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart new file mode 100644 index 0000000000..1fc01bb8e5 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart @@ -0,0 +1,123 @@ +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/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/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 VideoViewerControls extends HookConsumerWidget { + final Duration hideTimerDuration; + + const VideoViewerControls({ + super.key, + this.hideTimerDuration = const Duration(seconds: 5), + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final assetIsVideo = ref.watch( + currentAssetNotifier.select((asset) => asset != null && asset.isVideo), + ); + bool showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); + final showBottomSheet = ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet)); + if (showBottomSheet) { + showControls = false; + } + final VideoPlaybackState state = ref.watch(videoPlaybackValueProvider.select((value) => value.state)); + + final cast = ref.watch(castProvider); + + // A timer to hide the controls + final hideTimer = useTimer( + hideTimerDuration, + () { + if (!context.mounted) { + return; + } + final state = ref.read(videoPlaybackValueProvider).state; + + // Do not hide on paused + if (state != VideoPlaybackState.paused && state != VideoPlaybackState.completed && assetIsVideo) { + ref.read(assetViewerProvider.notifier).setControls(false); + } + }, + ); + final showBuffering = state == VideoPlaybackState.buffering && !cast.isCasting; + + /// Shows the controls and starts the timer to hide them + void showControlsAndStartHideTimer() { + hideTimer.reset(); + ref.read(assetViewerProvider.notifier).setControls(true); + } + + // When we change position, show or hide timer + ref.listen(videoPlayerControlsProvider.select((v) => v.position), (previous, next) { + showControlsAndStartHideTimer(); + }); + + /// 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(currentAssetNotifier); + if (asset == null) { + return; + } + // ref.read(castProvider.notifier).loadMedia(asset, true); + } + return; + } + + if (state == VideoPlaybackState.playing) { + ref.read(videoPlayerControlsProvider.notifier).pause(); + } else if (state == VideoPlaybackState.completed) { + ref.read(videoPlayerControlsProvider.notifier).restart(); + } else { + ref.read(videoPlayerControlsProvider.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(assetViewerProvider.notifier).setControls(false), + child: CenterPlayButton( + backgroundColor: Colors.black54, + iconColor: Colors.white, + isFinished: state == VideoPlaybackState.completed, + isPlaying: + state == VideoPlaybackState.playing || (cast.isCasting && cast.castState == CastState.playing), + show: assetIsVideo && showControls, + onPressed: togglePlay, + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart new file mode 100644 index 0000000000..520111070f --- /dev/null +++ b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart @@ -0,0 +1,232 @@ +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/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; + +class BackupToggleButton extends ConsumerStatefulWidget { + final VoidCallback onStart; + final VoidCallback onStop; + + const BackupToggleButton({ + super.key, + required this.onStart, + required this.onStop, + }); + + @override + ConsumerState createState() => BackupToggleButtonState(); +} + +class BackupToggleButtonState extends ConsumerState with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _gradientAnimation; + bool _isEnabled = false; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(seconds: 8), + vsync: this, + ); + + _gradientAnimation = Tween(begin: 0, end: 1).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); + + _isEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + Future _onToggle(bool value) async { + await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.enableBackup, value); + + setState(() { + _isEnabled = value; + }); + + if (value) { + widget.onStart.call(); + } else { + widget.onStop.call(); + } + } + + @override + Widget build(BuildContext context) { + final enqueueCount = ref.watch( + driftBackupProvider.select((state) => state.enqueueCount), + ); + + final enqueueTotalCount = ref.watch( + driftBackupProvider.select((state) => state.enqueueTotalCount), + ); + + final isCanceling = ref.watch( + driftBackupProvider.select((state) => state.isCanceling), + ); + + final uploadTasks = ref.watch( + driftBackupProvider.select((state) => state.uploadItems), + ); + + final isUploading = uploadTasks.isNotEmpty; + + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final gradientColors = [ + Color.lerp( + context.primaryColor.withValues(alpha: 0.5), + context.primaryColor.withValues(alpha: 0.3), + _gradientAnimation.value, + )!, + Color.lerp( + context.primaryColor.withValues(alpha: 0.2), + context.primaryColor.withValues(alpha: 0.4), + _gradientAnimation.value, + )!, + Color.lerp( + context.primaryColor.withValues(alpha: 0.3), + context.primaryColor.withValues(alpha: 0.5), + _gradientAnimation.value, + )!, + ]; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20)), + gradient: LinearGradient( + colors: gradientColors, + stops: const [0.0, 0.5, 1.0], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: context.primaryColor.withValues(alpha: 0.1), + blurRadius: 12, + offset: const Offset(0, 2), + ), + ], + ), + child: Container( + margin: const EdgeInsets.all(1.5), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(18.5)), + color: context.colorScheme.surfaceContainerLow, + ), + child: Material( + color: context.colorScheme.surfaceContainerLow, + borderRadius: const BorderRadius.all(Radius.circular(20.5)), + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(20.5)), + onTap: () => isCanceling ? null : _onToggle(!_isEnabled), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + context.primaryColor.withValues(alpha: 0.2), + context.primaryColor.withValues(alpha: 0.1), + ], + ), + ), + child: isUploading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : Icon( + Icons.cloud_upload_outlined, + color: context.primaryColor, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "enable_backup".t(context: context), + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ), + ], + ), + if (enqueueCount != enqueueTotalCount) + Text( + "queue_status".t( + context: context, + args: { + 'count': enqueueCount.toString(), + 'total': enqueueTotalCount.toString(), + }, + ), + style: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), + ), + if (isCanceling) + Row( + children: [ + Text( + "canceling".t(), + style: context.textTheme.labelLarge, + ), + const SizedBox(width: 4), + SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + backgroundColor: context.colorScheme.onSurface.withValues(alpha: 0.2), + ), + ), + ], + ), + ], + ), + ), + Switch.adaptive( + value: _isEnabled, + onChanged: (value) => isCanceling ? null : _onToggle(value), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart new file mode 100644 index 0000000000..76243cf803 --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_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/share_link_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; + +class ArchiveBottomSheet extends ConsumerWidget { + const ArchiveBottomSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final multiselect = ref.watch(multiSelectProvider); + final isTrashEnable = ref.watch( + serverInfoProvider.select((state) => state.serverFeatures.trash), + ); + + return BaseBottomSheet( + initialChildSize: 0.25, + maxChildSize: 0.4, + shouldCloseOnMinExtent: false, + actions: [ + const ShareActionButton(source: ActionSource.timeline), + if (multiselect.hasRemote) ...[ + const ShareLinkActionButton(source: ActionSource.timeline), + const UnArchiveActionButton(source: ActionSource.timeline), + const FavoriteActionButton(source: ActionSource.timeline), + const DownloadActionButton(source: ActionSource.timeline), + isTrashEnable + ? const TrashActionButton(source: ActionSource.timeline) + : const DeletePermanentActionButton( + source: ActionSource.timeline, + ), + const EditDateTimeActionButton(), + const EditLocationActionButton(source: ActionSource.timeline), + const MoveToLockFolderActionButton( + source: ActionSource.timeline, + ), + const StackActionButton(source: ActionSource.timeline), + ], + if (multiselect.hasLocal) ...[ + const DeleteLocalActionButton(source: ActionSource.timeline), + const UploadActionButton(source: ActionSource.timeline), + ], + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart new file mode 100644 index 0000000000..b3e71567e0 --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; + +class BaseBottomSheet extends ConsumerStatefulWidget { + final List actions; + final DraggableScrollableController? controller; + final List? slivers; + final double initialChildSize; + final double minChildSize; + final double maxChildSize; + final bool expand; + final bool shouldCloseOnMinExtent; + final bool resizeOnScroll; + final Color? backgroundColor; + + const BaseBottomSheet({ + super.key, + required this.actions, + this.slivers, + this.controller, + this.initialChildSize = 0.35, + this.minChildSize = 0.15, + this.maxChildSize = 0.65, + this.expand = true, + this.shouldCloseOnMinExtent = true, + this.resizeOnScroll = true, + this.backgroundColor, + }); + + @override + ConsumerState createState() => _BaseDraggableScrollableSheetState(); +} + +class _BaseDraggableScrollableSheetState extends ConsumerState { + late DraggableScrollableController _controller; + + @override + void initState() { + super.initState(); + _controller = widget.controller ?? DraggableScrollableController(); + } + + @override + Widget build(BuildContext context) { + ref.listen(timelineStateProvider, (previous, next) { + if (!widget.resizeOnScroll) { + return; + } + + if (previous?.isInteracting != true && next.isInteracting) { + _controller.animateTo( + widget.minChildSize, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ); + } + }); + + return DraggableScrollableSheet( + controller: _controller, + initialChildSize: widget.initialChildSize, + minChildSize: widget.minChildSize, + maxChildSize: widget.maxChildSize, + snap: false, + expand: widget.expand, + shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent, + builder: (BuildContext context, ScrollController scrollController) { + return Card( + color: widget.backgroundColor ?? context.colorScheme.surfaceContainerHigh, + borderOnForeground: false, + clipBehavior: Clip.antiAlias, + elevation: 6.0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(18)), + ), + margin: const EdgeInsets.symmetric(horizontal: 0), + child: CustomScrollView( + controller: scrollController, + slivers: [ + SliverToBoxAdapter( + child: Column( + children: [ + const SizedBox(height: 10), + const _DragHandle(), + const SizedBox(height: 14), + if (widget.actions.isNotEmpty) + SizedBox( + height: 115, + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: widget.actions, + ), + ), + if (widget.actions.isNotEmpty) ...[ + const Divider(indent: 16, endIndent: 16), + const SizedBox(height: 16), + ], + ], + ), + ), + ...(widget.slivers ?? []), + ], + ), + ); + }, + ); + } +} + +class _DragHandle extends StatelessWidget { + const _DragHandle(); + + @override + Widget build(BuildContext context) { + return Container( + height: 6, + width: 32, + decoration: BoxDecoration( + color: context.themeData.dividerColor.lighten(amount: 0.6), + borderRadius: const BorderRadius.all(Radius.circular(20)), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart new file mode 100644 index 0000000000..3f3f933745 --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_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/share_link_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; + +class FavoriteBottomSheet extends ConsumerWidget { + const FavoriteBottomSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final multiselect = ref.watch(multiSelectProvider); + final isTrashEnable = ref.watch( + serverInfoProvider.select((state) => state.serverFeatures.trash), + ); + + return BaseBottomSheet( + initialChildSize: 0.25, + maxChildSize: 0.4, + shouldCloseOnMinExtent: false, + actions: [ + const ShareActionButton(source: ActionSource.timeline), + if (multiselect.hasRemote) ...[ + const ShareLinkActionButton(source: ActionSource.timeline), + const UnFavoriteActionButton(source: ActionSource.timeline), + const ArchiveActionButton(source: ActionSource.timeline), + const DownloadActionButton(source: ActionSource.timeline), + isTrashEnable + ? const TrashActionButton(source: ActionSource.timeline) + : const DeletePermanentActionButton( + source: ActionSource.timeline, + ), + const EditDateTimeActionButton(), + const EditLocationActionButton(source: ActionSource.timeline), + const MoveToLockFolderActionButton( + source: ActionSource.timeline, + ), + const StackActionButton(source: ActionSource.timeline), + ], + if (multiselect.hasLocal) ...[ + const DeleteLocalActionButton(source: ActionSource.timeline), + const UploadActionButton(source: ActionSource.timeline), + ], + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart new file mode 100644 index 0000000000..d338cfa833 --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -0,0 +1,108 @@ +import 'package:easy_localization/easy_localization.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/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_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/share_link_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class GeneralBottomSheet extends ConsumerWidget { + const GeneralBottomSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final multiselect = ref.watch(multiSelectProvider); + final isTrashEnable = ref.watch( + serverInfoProvider.select((state) => state.serverFeatures.trash), + ); + + Future addAssetsToAlbum(RemoteAlbum album) async { + final selectedAssets = multiselect.selectedAssets; + if (selectedAssets.isEmpty) { + return; + } + + final addedCount = await ref.read(remoteAlbumProvider.notifier).addAssets( + album.id, + selectedAssets.map((e) => (e as RemoteAsset).id).toList(), + ); + + if (addedCount != selectedAssets.length) { + ImmichToast.show( + context: context, + msg: 'add_to_album_bottom_sheet_already_exists'.tr( + namedArgs: {"album": album.name}, + ), + ); + } else { + ImmichToast.show( + context: context, + msg: 'add_to_album_bottom_sheet_added'.tr( + namedArgs: {"album": album.name}, + ), + ); + } + + ref.read(multiSelectProvider.notifier).reset(); + } + + return BaseBottomSheet( + initialChildSize: 0.45, + maxChildSize: 0.85, + shouldCloseOnMinExtent: false, + actions: [ + const ShareActionButton(source: ActionSource.timeline), + if (multiselect.hasRemote) ...[ + const ShareLinkActionButton(source: ActionSource.timeline), + const ArchiveActionButton(source: ActionSource.timeline), + const FavoriteActionButton(source: ActionSource.timeline), + const DownloadActionButton(source: ActionSource.timeline), + isTrashEnable + ? const TrashActionButton(source: ActionSource.timeline) + : const DeletePermanentActionButton( + source: ActionSource.timeline, + ), + const DeleteActionButton(source: ActionSource.timeline), + if (multiselect.hasLocal || multiselect.hasMerged) ...[ + const DeleteLocalActionButton(source: ActionSource.timeline), + ], + const EditDateTimeActionButton(), + const EditLocationActionButton(source: ActionSource.timeline), + const MoveToLockFolderActionButton( + source: ActionSource.timeline, + ), + const StackActionButton(source: ActionSource.timeline), + ], + if (multiselect.hasLocal) ...[ + const DeleteLocalActionButton(source: ActionSource.timeline), + const UploadActionButton(source: ActionSource.timeline), + ], + ], + slivers: [ + const AddToAlbumHeader(), + AlbumSelector( + onAlbumSelected: addAssetsToAlbum, + ), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart new file mode 100644 index 0000000000..b1e87dfaea --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_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/bottom_sheet/base_bottom_sheet.widget.dart'; + +class LocalAlbumBottomSheet extends ConsumerWidget { + const LocalAlbumBottomSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return const BaseBottomSheet( + initialChildSize: 0.25, + maxChildSize: 0.4, + shouldCloseOnMinExtent: false, + actions: [ + ShareActionButton(source: ActionSource.timeline), + DeleteLocalActionButton(source: ActionSource.timeline), + UploadActionButton(source: ActionSource.timeline), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart new file mode 100644 index 0000000000..a644e6a035 --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; + +class LockedFolderBottomSheet extends ConsumerWidget { + const LockedFolderBottomSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return const BaseBottomSheet( + initialChildSize: 0.25, + maxChildSize: 0.4, + shouldCloseOnMinExtent: false, + actions: [ + ShareActionButton(source: ActionSource.timeline), + DownloadActionButton(source: ActionSource.timeline), + DeletePermanentActionButton(source: ActionSource.timeline), + RemoveFromLockFolderActionButton(source: ActionSource.timeline), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart new file mode 100644 index 0000000000..5e4dae34bc --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; + +class PartnerDetailBottomSheet extends ConsumerWidget { + const PartnerDetailBottomSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return const BaseBottomSheet( + initialChildSize: 0.25, + maxChildSize: 0.4, + shouldCloseOnMinExtent: false, + actions: [ + ShareActionButton(source: ActionSource.timeline), + DownloadActionButton(source: ActionSource.timeline), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart new file mode 100644 index 0000000000..ff77c79906 --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_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/share_link_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; + +class RemoteAlbumBottomSheet extends ConsumerWidget { + final RemoteAlbum album; + const RemoteAlbumBottomSheet({super.key, required this.album}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final multiselect = ref.watch(multiSelectProvider); + final isTrashEnable = ref.watch( + serverInfoProvider.select((state) => state.serverFeatures.trash), + ); + + return BaseBottomSheet( + initialChildSize: 0.25, + maxChildSize: 0.4, + shouldCloseOnMinExtent: false, + actions: [ + const ShareActionButton(source: ActionSource.timeline), + if (multiselect.hasRemote) ...[ + const ShareLinkActionButton(source: ActionSource.timeline), + const ArchiveActionButton(source: ActionSource.timeline), + const FavoriteActionButton(source: ActionSource.timeline), + const DownloadActionButton(source: ActionSource.timeline), + isTrashEnable + ? const TrashActionButton(source: ActionSource.timeline) + : const DeletePermanentActionButton( + source: ActionSource.timeline, + ), + const EditDateTimeActionButton(), + const EditLocationActionButton(source: ActionSource.timeline), + const MoveToLockFolderActionButton( + source: ActionSource.timeline, + ), + const StackActionButton(source: ActionSource.timeline), + ], + if (multiselect.hasLocal) ...[ + const DeleteLocalActionButton(source: ActionSource.timeline), + const UploadActionButton(source: ActionSource.timeline), + ], + RemoveFromAlbumActionButton( + source: ActionSource.timeline, + albumId: album.id, + ), + ], + ); + } +} 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 new file mode 100644 index 0000000000..9f8216c4ed --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart'; + +class TrashBottomBar extends ConsumerWidget { + const TrashBottomBar({super.key}); + + @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), + ], + ), + ), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/images/full_image.widget.dart b/mobile/lib/presentation/widgets/images/full_image.widget.dart new file mode 100644 index 0000000000..77ea996b89 --- /dev/null +++ b/mobile/lib/presentation/widgets/images/full_image.widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; +import 'package:octo_image/octo_image.dart'; + +class FullImage extends StatelessWidget { + const FullImage( + this.asset, { + required this.size, + this.fit = BoxFit.cover, + this.placeholder = const ThumbnailPlaceholder(), + super.key, + }); + + final BaseAsset asset; + final Size size; + final Widget? placeholder; + final BoxFit fit; + + @override + Widget build(BuildContext context) { + final provider = getFullImageProvider(asset, size: size); + return OctoImage( + fadeInDuration: const Duration(milliseconds: 0), + fadeOutDuration: const Duration(milliseconds: 100), + placeholderBuilder: placeholder != null ? (_) => placeholder! : null, + image: provider, + width: size.width, + height: size.height, + fit: fit, + errorBuilder: (context, error, stackTrace) { + provider.evict(); + return const Icon(Icons.image_not_supported_outlined, size: 32); + }, + ); + } +} diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart new file mode 100644 index 0000000000..e4effd0804 --- /dev/null +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -0,0 +1,74 @@ +import 'package:flutter/widgets.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/setting.model.dart'; +import 'package:immich_mobile/domain/services/setting.service.dart'; +import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; + +ImageProvider getFullImageProvider( + BaseAsset asset, { + Size size = const Size(1080, 1920), +}) { + // Create new provider and cache it + final ImageProvider provider; + if (_shouldUseLocalAsset(asset)) { + final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; + provider = LocalFullImageProvider( + id: id, + name: asset.name, + size: size, + type: asset.type, + ); + } else { + final String assetId; + if (asset is LocalAsset && asset.hasRemote) { + assetId = asset.remoteId!; + } else if (asset is RemoteAsset) { + assetId = asset.id; + } else { + throw ArgumentError("Unsupported asset type: ${asset.runtimeType}"); + } + provider = RemoteFullImageProvider(assetId: assetId); + } + + return provider; +} + +ImageProvider getThumbnailImageProvider({ + BaseAsset? asset, + String? remoteId, + Size size = const Size.square(256), +}) { + assert( + asset != null || remoteId != null, + 'Either asset or remoteId must be provided', + ); + + if (remoteId != null) { + return RemoteThumbProvider(assetId: remoteId); + } + + if (_shouldUseLocalAsset(asset!)) { + final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; + return LocalThumbProvider( + id: id, + updatedAt: asset.updatedAt, + name: asset.name, + size: size, + ); + } + + final String assetId; + if (asset is LocalAsset && asset.hasRemote) { + assetId = asset.remoteId!; + } else if (asset is RemoteAsset) { + assetId = asset.id; + } else { + throw ArgumentError("Unsupported asset type: ${asset.runtimeType}"); + } + + return RemoteThumbProvider(assetId: assetId); +} + +bool _shouldUseLocalAsset(BaseAsset asset) => + asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)); diff --git a/mobile/lib/presentation/widgets/images/local_album_thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/local_album_thumbnail.widget.dart new file mode 100644 index 0000000000..dcf0f28527 --- /dev/null +++ b/mobile/lib/presentation/widgets/images/local_album_thumbnail.widget.dart @@ -0,0 +1,54 @@ +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/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; + +class LocalAlbumThumbnail extends ConsumerWidget { + const LocalAlbumThumbnail({ + super.key, + required this.albumId, + }); + + final String albumId; + @override + Widget build(BuildContext context, WidgetRef ref) { + final localAlbumThumbnail = ref.watch(localAlbumThumbnailProvider(albumId)); + return localAlbumThumbnail.when( + data: (data) { + if (data == null) { + return Container( + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainer, + borderRadius: const BorderRadius.all(Radius.circular(16)), + border: Border.all( + color: context.colorScheme.outline.withAlpha(50), + width: 1, + ), + ), + child: Icon( + Icons.collections, + size: 24, + color: context.primaryColor, + ), + ); + } + + return ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: Thumbnail( + asset: data, + ), + ); + }, + error: (error, stack) { + return const Icon(Icons.error, size: 24); + }, + loading: () => const SizedBox( + width: 24, + height: 24, + child: Center(child: CircularProgressIndicator()), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart new file mode 100644 index 0000000000..41bc19ba57 --- /dev/null +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -0,0 +1,238 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/setting.model.dart'; +import 'package:immich_mobile/domain/services/setting.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; +import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart'; +import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart'; +import 'package:logging/logging.dart'; + +class LocalThumbProvider extends ImageProvider { + final AssetMediaRepository _assetMediaRepository = const AssetMediaRepository(); + final CacheManager? cacheManager; + + final String id; + final DateTime updatedAt; + final String name; + final Size size; + + const LocalThumbProvider({ + required this.id, + required this.updatedAt, + required this.name, + this.size = const Size.square(kTimelineFixedTileExtent), + this.cacheManager, + }); + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter loadImage( + LocalThumbProvider key, + ImageDecoderCallback decode, + ) { + final cache = cacheManager ?? ThumbnailImageCacheManager(); + return MultiFrameImageStreamCompleter( + codec: _codec(key, cache, decode), + scale: 1.0, + informationCollector: () => [ + DiagnosticsProperty('Image provider', this), + DiagnosticsProperty('Id', key.id), + DiagnosticsProperty('Updated at', key.updatedAt), + DiagnosticsProperty('Name', key.name), + DiagnosticsProperty('Size', key.size), + ], + ); + } + + Future _codec( + LocalThumbProvider key, + CacheManager cache, + ImageDecoderCallback decode, + ) async { + final cacheKey = '${key.id}-${key.updatedAt}-${key.size.width}x${key.size.height}'; + + final fileFromCache = await cache.getFileFromCache(cacheKey); + if (fileFromCache != null) { + try { + final buffer = await ImmutableBuffer.fromFilePath(fileFromCache.file.path); + return decode(buffer); + } catch (_) {} + } + + final thumbnailBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size); + if (thumbnailBytes == null) { + PaintingBinding.instance.imageCache.evict(key); + throw StateError( + "Loading thumb for local photo ${key.name} failed", + ); + } + + final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes); + unawaited(cache.putFile(cacheKey, thumbnailBytes)); + return decode(buffer); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is LocalThumbProvider) { + return id == other.id && updatedAt == other.updatedAt; + } + return false; + } + + @override + int get hashCode => id.hashCode ^ updatedAt.hashCode; +} + +class LocalFullImageProvider extends ImageProvider { + final AssetMediaRepository _assetMediaRepository = const AssetMediaRepository(); + final StorageRepository _storageRepository = const StorageRepository(); + + final String id; + final String name; + final Size size; + final AssetType type; + + const LocalFullImageProvider({ + required this.id, + required this.name, + required this.size, + required this.type, + }); + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter loadImage( + LocalFullImageProvider key, + ImageDecoderCallback decode, + ) { + return MultiImageStreamCompleter( + codec: _codec(key, decode), + scale: 1.0, + informationCollector: () sync* { + yield ErrorDescription(name); + }, + ); + } + + // Streams in each stage of the image as we ask for it + Stream _codec( + LocalFullImageProvider key, + ImageDecoderCallback decode, + ) async* { + try { + switch (key.type) { + case AssetType.image: + yield* _decodeProgressive(key, decode); + break; + case AssetType.video: + final codec = await _getThumbnailCodec(key, decode); + if (codec == null) { + throw StateError("Failed to load preview for ${key.name}"); + } + yield codec; + break; + case AssetType.other: + case AssetType.audio: + throw StateError('Unsupported asset type ${key.type}'); + } + } catch (error, stack) { + Logger('ImmichLocalImageProvider').severe('Error loading local image ${key.name}', error, stack); + throw const ImageLoadingException( + 'Could not load image from local storage', + ); + } + } + + Future _getThumbnailCodec( + LocalFullImageProvider key, + ImageDecoderCallback decode, + ) async { + final thumbBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size); + if (thumbBytes == null) { + return null; + } + final buffer = await ImmutableBuffer.fromUint8List(thumbBytes); + return decode(buffer); + } + + Stream _decodeProgressive( + LocalFullImageProvider key, + ImageDecoderCallback decode, + ) async* { + final file = await _storageRepository.getFileForAsset(key.id); + if (file == null) { + throw StateError("Opening file for asset ${key.name} failed"); + } + + final fileSize = await file.length(); + final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio; + final isLargeFile = fileSize > 20 * 1024 * 1024; // 20MB + final isHEIC = file.path.toLowerCase().contains(RegExp(r'\.(heic|heif)$')); + final isProgressive = isLargeFile || (isHEIC && !Platform.isIOS); + + if (isProgressive) { + try { + final progressiveMultiplier = devicePixelRatio >= 3.0 ? 1.3 : 1.2; + final size = Size( + (key.size.width * progressiveMultiplier).clamp(256, 1024), + (key.size.height * progressiveMultiplier).clamp(256, 1024), + ); + final mediumThumb = await _assetMediaRepository.getThumbnail(key.id, size: size); + if (mediumThumb != null) { + final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb); + yield await decode(mediumBuffer); + } + } catch (_) {} + } + + // Load original only when the file is smaller or if the user wants to load original images + // Or load a slightly larger image for progressive loading + if (isProgressive && !(AppSetting.get(Setting.loadOriginal))) { + final progressiveMultiplier = devicePixelRatio >= 3.0 ? 2.0 : 1.6; + final size = Size( + (key.size.width * progressiveMultiplier).clamp(512, 2048), + (key.size.height * progressiveMultiplier).clamp(512, 2048), + ); + final highThumb = await _assetMediaRepository.getThumbnail(key.id, size: size); + if (highThumb != null) { + final highBuffer = await ImmutableBuffer.fromUint8List(highThumb); + yield await decode(highBuffer); + } + return; + } + + final buffer = await ImmutableBuffer.fromFilePath(file.path); + yield await decode(buffer); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is LocalFullImageProvider) { + return id == other.id && size == other.size && type == other.type && name == other.name; + } + return false; + } + + @override + int get hashCode => id.hashCode ^ size.hashCode ^ type.hashCode ^ name.hashCode; +} diff --git a/mobile/lib/presentation/widgets/images/local_thumb_provider.dart b/mobile/lib/presentation/widgets/images/local_thumb_provider.dart deleted file mode 100644 index 607057cf44..0000000000 --- a/mobile/lib/presentation/widgets/images/local_thumb_provider.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'dart:async'; -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:immich_mobile/domain/interfaces/asset_media.interface.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; -import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart'; - -class LocalThumbProvider extends ImageProvider { - final IAssetMediaRepository _assetMediaRepository = - const AssetMediaRepository(); - final CacheManager? cacheManager; - - final LocalAsset asset; - final double height; - final double width; - - LocalThumbProvider({ - required this.asset, - this.height = kTimelineFixedTileExtent, - this.width = kTimelineFixedTileExtent, - this.cacheManager, - }); - - @override - Future obtainKey( - ImageConfiguration configuration, - ) { - return SynchronousFuture(this); - } - - @override - ImageStreamCompleter loadImage( - LocalThumbProvider key, - ImageDecoderCallback decode, - ) { - final cache = cacheManager ?? ThumbnailImageCacheManager(); - return MultiFrameImageStreamCompleter( - codec: _codec(key, cache, decode), - scale: 1.0, - informationCollector: () => [ - DiagnosticsProperty('Image provider', this), - DiagnosticsProperty('Asset', key.asset), - ], - ); - } - - Future _codec( - LocalThumbProvider key, - CacheManager cache, - ImageDecoderCallback decode, - ) async { - final cacheKey = '${key.asset.id}-${key.asset.updatedAt}-${width}x$height'; - - final fileFromCache = await cache.getFileFromCache(cacheKey); - if (fileFromCache != null) { - try { - final buffer = - await ImmutableBuffer.fromFilePath(fileFromCache.file.path); - return await decode(buffer); - } catch (_) {} - } - - final thumbnailBytes = await _assetMediaRepository.getThumbnail( - key.asset.id, - size: Size(key.width, key.height), - ); - if (thumbnailBytes == null) { - PaintingBinding.instance.imageCache.evict(key); - throw StateError( - "Loading thumb for local photo ${key.asset.name} failed", - ); - } - - final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes); - unawaited(cache.putFile(cacheKey, thumbnailBytes)); - return decode(buffer); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is LocalThumbProvider) { - return asset.id == other.asset.id && - asset.updatedAt == other.asset.updatedAt; - } - return false; - } - - @override - int get hashCode => asset.id.hashCode ^ asset.updatedAt.hashCode; -} diff --git a/mobile/lib/presentation/widgets/images/remote_image_provider.dart b/mobile/lib/presentation/widgets/images/remote_image_provider.dart new file mode 100644 index 0000000000..14d13a08d8 --- /dev/null +++ b/mobile/lib/presentation/widgets/images/remote_image_provider.dart @@ -0,0 +1,142 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:immich_mobile/domain/models/setting.model.dart'; +import 'package:immich_mobile/domain/services/setting.service.dart'; +import 'package:immich_mobile/providers/image/cache/image_loader.dart'; +import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; + +class RemoteThumbProvider extends ImageProvider { + final String assetId; + final CacheManager? cacheManager; + + const RemoteThumbProvider({ + required this.assetId, + this.cacheManager, + }); + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter loadImage( + RemoteThumbProvider key, + ImageDecoderCallback decode, + ) { + final cache = cacheManager ?? RemoteImageCacheManager(); + final chunkController = StreamController(); + return MultiFrameImageStreamCompleter( + codec: _codec(key, cache, decode, chunkController), + scale: 1.0, + chunkEvents: chunkController.stream, + informationCollector: () => [ + DiagnosticsProperty('Image provider', this), + DiagnosticsProperty('Asset Id', key.assetId), + ], + ); + } + + Future _codec( + RemoteThumbProvider key, + CacheManager cache, + ImageDecoderCallback decode, + StreamController chunkController, + ) async { + final preview = getThumbnailUrlForRemoteId( + key.assetId, + ); + + return ImageLoader.loadImageFromCache( + preview, + cache: cache, + decode: decode, + chunkEvents: chunkController, + ).whenComplete(chunkController.close); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is RemoteThumbProvider) { + return assetId == other.assetId; + } + + return false; + } + + @override + int get hashCode => assetId.hashCode; +} + +class RemoteFullImageProvider extends ImageProvider { + final String assetId; + final CacheManager? cacheManager; + + const RemoteFullImageProvider({ + required this.assetId, + this.cacheManager, + }); + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter loadImage( + RemoteFullImageProvider key, + ImageDecoderCallback decode, + ) { + final cache = cacheManager ?? RemoteImageCacheManager(); + final chunkEvents = StreamController(); + return MultiImageStreamCompleter( + codec: _codec(key, cache, decode, chunkEvents), + scale: 1.0, + chunkEvents: chunkEvents.stream, + ); + } + + Stream _codec( + RemoteFullImageProvider key, + CacheManager cache, + ImageDecoderCallback decode, + StreamController chunkController, + ) async* { + yield await ImageLoader.loadImageFromCache( + getPreviewUrlForRemoteId(key.assetId), + cache: cache, + decode: decode, + chunkEvents: chunkController, + ); + + if (AppSetting.get(Setting.loadOriginal)) { + yield await ImageLoader.loadImageFromCache( + getOriginalUrlForRemoteId(key.assetId), + cache: cache, + decode: decode, + chunkEvents: chunkController, + ); + } + await chunkController.close(); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is RemoteFullImageProvider) { + return assetId == other.assetId; + } + + return false; + } + + @override + int get hashCode => assetId.hashCode; +} diff --git a/mobile/lib/presentation/widgets/images/remote_thumb_provider.dart b/mobile/lib/presentation/widgets/images/remote_thumb_provider.dart deleted file mode 100644 index c9561ee156..0000000000 --- a/mobile/lib/presentation/widgets/images/remote_thumb_provider.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'dart:async'; -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/painting.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; -import 'package:immich_mobile/providers/image/cache/image_loader.dart'; -import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; - -class RemoteThumbProvider extends ImageProvider { - final String assetId; - final double height; - final double width; - final CacheManager? cacheManager; - - RemoteThumbProvider({ - required this.assetId, - this.height = kTimelineFixedTileExtent, - this.width = kTimelineFixedTileExtent, - this.cacheManager, - }); - - @override - Future obtainKey( - ImageConfiguration configuration, - ) { - return SynchronousFuture(this); - } - - @override - ImageStreamCompleter loadImage( - RemoteThumbProvider key, - ImageDecoderCallback decode, - ) { - final cache = cacheManager ?? ThumbnailImageCacheManager(); - final chunkController = StreamController(); - return MultiFrameImageStreamCompleter( - codec: _codec(key, cache, decode, chunkController), - scale: 1.0, - chunkEvents: chunkController.stream, - informationCollector: () => [ - DiagnosticsProperty('Image provider', this), - DiagnosticsProperty('Asset Id', key.assetId), - ], - ); - } - - Future _codec( - RemoteThumbProvider key, - CacheManager cache, - ImageDecoderCallback decode, - StreamController chunkController, - ) async { - final preview = getThumbnailUrlForRemoteId( - key.assetId, - ); - - return ImageLoader.loadImageFromCache( - preview, - cache: cache, - decode: decode, - chunkEvents: chunkController, - ).whenComplete(chunkController.close); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is RemoteThumbProvider) { - return assetId == other.assetId; - } - - return false; - } - - @override - int get hashCode => assetId.hashCode; -} diff --git a/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart b/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart index 308c92e968..cd286a4cdf 100644 --- a/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart +++ b/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart @@ -8,7 +8,7 @@ import 'package:thumbhash/thumbhash.dart'; class ThumbHashProvider extends ImageProvider { final String thumbHash; - ThumbHashProvider({ + const ThumbHashProvider({ required this.thumbHash, }); diff --git a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart index e9648ab06e..80f6af617c 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_thumb_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_thumb_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart'; @@ -10,43 +9,25 @@ import 'package:octo_image/octo_image.dart'; class Thumbnail extends StatelessWidget { const Thumbnail({ - required this.asset, + this.asset, + this.remoteId, this.size = const Size.square(256), this.fit = BoxFit.cover, super.key, - }); + }) : assert( + asset != null || remoteId != null, + 'Either asset or remoteId must be provided', + ); - final BaseAsset asset; + final BaseAsset? asset; + final String? remoteId; final Size size; final BoxFit fit; - static ImageProvider imageProvider({ - required BaseAsset asset, - Size size = const Size.square(256), - }) { - if (asset is LocalAsset) { - return LocalThumbProvider( - asset: asset, - height: size.height, - width: size.width, - ); - } - - if (asset is Asset) { - return RemoteThumbProvider( - assetId: asset.id, - height: size.height, - width: size.width, - ); - } - - throw ArgumentError("Unsupported asset type: ${asset.runtimeType}"); - } - @override Widget build(BuildContext context) { - final thumbHash = asset is Asset ? (asset as Asset).thumbHash : null; - final provider = imageProvider(asset: asset, size: size); + final thumbHash = asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null; + final provider = getThumbnailImageProvider(asset: asset, remoteId: remoteId, size: size); return OctoImage.fromSet( image: provider, @@ -89,8 +70,7 @@ OctoErrorBuilder _blurHashErrorBuilder( BoxFit? fit, }) => (context, e, s) { - Logger("ImThumbnail") - .warning("Error loading thumbnail for ${asset?.name}", e, s); + Logger("ImThumbnail").warning("Error loading thumbnail for ${asset?.name}", e, s); provider?.evict(); return Stack( alignment: Alignment.center, diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index eb64b2dcd3..ce4e50cbd5 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -1,13 +1,21 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/duration_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -class ThumbnailTile extends StatelessWidget { +class ThumbnailTile extends ConsumerWidget { const ThumbnailTile( this.asset, { this.size = const Size.square(256), this.fit = BoxFit.cover, this.showStorageIndicator = true, + this.lockSelection = false, + this.heroOffset, super.key, }); @@ -15,65 +23,179 @@ class ThumbnailTile extends StatelessWidget { final Size size; final BoxFit fit; final bool showStorageIndicator; + final bool lockSelection; + final int? heroOffset; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final heroIndex = heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0; + + final assetContainerColor = + context.isDarkTheme ? context.primaryColor.darken(amount: 0.4) : context.primaryColor.lighten(amount: 0.75); + + final isSelected = ref.watch( + multiSelectProvider.select( + (multiselect) => multiselect.selectedAssets.contains(asset), + ), + ); + + final borderStyle = lockSelection + ? BoxDecoration( + color: context.colorScheme.surfaceContainerHighest, + border: Border.all( + color: context.colorScheme.surfaceContainerHighest, + width: 6, + ), + ) + : isSelected + ? BoxDecoration( + color: assetContainerColor, + border: Border.all(color: assetContainerColor, width: 6), + ) + : const BoxDecoration(); + + final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null; + return Stack( children: [ - Positioned.fill(child: Thumbnail(asset: asset, fit: fit, size: size)), - if (asset.isVideo) - Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.only(right: 10.0, top: 6.0), - child: _VideoIndicator(asset.durationInSeconds ?? 0), + AnimatedContainer( + duration: Durations.short4, + curve: Curves.decelerate, + decoration: borderStyle, + child: ClipRRect( + borderRadius: + isSelected || lockSelection ? const BorderRadius.all(Radius.circular(15.0)) : BorderRadius.zero, + child: Stack( + children: [ + Positioned.fill( + child: Hero( + tag: '${asset.heroTag}_$heroIndex', + child: Thumbnail( + asset: asset, + fit: fit, + size: size, + ), + ), + ), + if (hasStack) + Align( + alignment: Alignment.topRight, + child: Padding( + padding: EdgeInsets.only( + right: 10.0, + top: asset.isVideo ? 24.0 : 6.0, + ), + child: const _TileOverlayIcon(Icons.burst_mode_rounded), + ), + ), + if (asset.isVideo) + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.only(right: 10.0, top: 6.0), + child: _VideoIndicator(asset.duration), + ), + ), + if (showStorageIndicator) + switch (asset.storage) { + AssetState.local => const Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.only(right: 10.0, bottom: 6.0), + child: _TileOverlayIcon(Icons.cloud_off_outlined), + ), + ), + AssetState.remote => const Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.only(right: 10.0, bottom: 6.0), + child: _TileOverlayIcon(Icons.cloud_outlined), + ), + ), + AssetState.merged => const Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.only(right: 10.0, bottom: 6.0), + child: _TileOverlayIcon(Icons.cloud_done_outlined), + ), + ), + }, + if (asset.isFavorite) + const Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: EdgeInsets.only(left: 10.0, bottom: 6.0), + child: _TileOverlayIcon(Icons.favorite_rounded), + ), + ), + ], ), ), - if (showStorageIndicator) - Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: const EdgeInsets.only(right: 10.0, bottom: 6.0), - child: _TileOverlayIcon( - switch (asset.storage) { - AssetState.local => Icons.cloud_off_outlined, - AssetState.remote => Icons.cloud_outlined, - AssetState.merged => Icons.cloud_done_outlined, - }, + ), + if (isSelected || lockSelection) + Padding( + padding: const EdgeInsets.all(3.0), + child: Align( + alignment: Alignment.topLeft, + child: _SelectionIndicator( + isSelected: isSelected, + isLocked: lockSelection, + color: lockSelection ? context.colorScheme.surfaceContainerHighest : assetContainerColor, ), ), ), - if (asset.isFavorite) - const Align( - alignment: Alignment.bottomLeft, - child: Padding( - padding: EdgeInsets.only(left: 10.0, bottom: 6.0), - child: _TileOverlayIcon(Icons.favorite_rounded), - ), - ), ], ); } } -class _VideoIndicator extends StatelessWidget { - final int durationInSeconds; - const _VideoIndicator(this.durationInSeconds); +class _SelectionIndicator extends StatelessWidget { + final bool isSelected; + final bool isLocked; + final Color? color; - String _formatDuration(int durationInSec) { - final int hours = durationInSec ~/ 3600; - final int minutes = (durationInSec % 3600) ~/ 60; - final int seconds = durationInSec % 60; + const _SelectionIndicator({ + required this.isSelected, + required this.isLocked, + this.color, + }); - final String minutesPadded = minutes.toString().padLeft(2, '0'); - final String secondsPadded = seconds.toString().padLeft(2, '0'); - - if (hours > 0) { - return "$hours:$minutesPadded:$secondsPadded"; // H:MM:SS + @override + Widget build(BuildContext context) { + if (isLocked) { + return DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + child: const Icon( + Icons.check_circle_rounded, + color: Colors.grey, + ), + ); + } else if (isSelected) { + return DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + child: Icon( + Icons.check_circle_rounded, + color: context.primaryColor, + ), + ); } else { - return "$minutesPadded:$secondsPadded"; // MM:SS + return const Icon( + Icons.circle_outlined, + color: Colors.white, + ); } } +} + +class _VideoIndicator extends StatelessWidget { + final Duration duration; + const _VideoIndicator(this.duration); @override Widget build(BuildContext context) { @@ -81,19 +203,19 @@ class _VideoIndicator extends StatelessWidget { spacing: 3, mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, - // CrossAxisAlignment.end looks more centered vertically than CrossAxisAlignment.center - crossAxisAlignment: CrossAxisAlignment.end, + // CrossAxisAlignment.start looks more centered vertically than CrossAxisAlignment.center + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _formatDuration(durationInSeconds), - style: TextStyle( + duration.format(), + style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, shadows: [ Shadow( blurRadius: 5.0, - color: Colors.black.withValues(alpha: 0.6), + color: Color.fromRGBO(0, 0, 0, 0.6), ), ], ), @@ -116,10 +238,10 @@ class _TileOverlayIcon extends StatelessWidget { color: Colors.white, size: 16, shadows: [ - Shadow( + const Shadow( blurRadius: 5.0, - color: Colors.black.withValues(alpha: 0.6), - offset: const Offset(0.0, 0.0), + color: Color.fromRGBO(0, 0, 0, 0.6), + offset: Offset(0.0, 0.0), ), ], ); diff --git a/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart b/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart new file mode 100644 index 0000000000..79e6288a72 --- /dev/null +++ b/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart @@ -0,0 +1,64 @@ +// 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/domain/models/memory.model.dart'; + +import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; + +class DriftMemoryBottomInfo extends StatelessWidget { + final DriftMemory memory; + final String title; + const DriftMemoryBottomInfo({ + super.key, + required this.memory, + required this.title, + }); + + @override + Widget build(BuildContext context) { + final df = DateFormat.yMMMMd(); + final fileCreatedDate = memory.assets.first.createdAt; + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: Colors.grey[400], + fontSize: 13.0, + fontWeight: FontWeight.w500, + ), + ), + Text( + df.format(fileCreatedDate), + style: const TextStyle( + color: Colors.white, + fontSize: 15.0, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + MaterialButton( + minWidth: 0, + onPressed: () { + context.maybePop(); + scrollToDateNotifierProvider.scrollToDate(fileCreatedDate); + }, + 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/presentation/widgets/memory/memory_card.widget.dart b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart new file mode 100644 index 0000000000..e69c848f45 --- /dev/null +++ b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart @@ -0,0 +1,146 @@ +import 'dart:ui'; + +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/presentation/widgets/asset_viewer/video_viewer.widget.dart'; +import 'package:immich_mobile/presentation/widgets/images/full_image.widget.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; + +class DriftMemoryCard extends StatelessWidget { + final RemoteAsset asset; + final String title; + final bool showTitle; + final Function()? onVideoEnded; + + const DriftMemoryCard({ + 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 FullImage( + asset, + fit: fit, + size: const Size(double.infinity, double.infinity), + ); + } else { + return SizedBox( + width: context.width, + height: context.height, + child: NativeVideoViewer( + key: ValueKey(asset.id), + asset: asset, + showControls: false, + playbackDelayFactor: 2, + image: FullImage( + asset, + size: Size(context.width, 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 RemoteAsset asset; + + const _BlurredBackdrop({required this.asset}); + + @override + Widget build(BuildContext context) { + final blurhash = useDriftBlurHashRef(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: getFullImageProvider( + asset, + size: Size(context.width, context.height), + ), + fit: BoxFit.cover, + ), + ), + child: Container( + color: Colors.black.withValues(alpha: 0.2), + ), + ), + ); + } + } +} diff --git a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart new file mode 100644 index 0000000000..4863b60aad --- /dev/null +++ b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart @@ -0,0 +1,111 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class DriftMemoryLane extends ConsumerWidget { + final List memories; + + const DriftMemoryLane({super.key, required this.memories}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return 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: (index) { + ref.read(hapticFeedbackProvider.notifier).heavyImpact(); + + if (memories[index].assets.isNotEmpty) { + final asset = memories[index].assets[0]; + ref.read(currentAssetNotifier.notifier).setAsset(asset); + + if (asset.isVideo) { + ref.read(videoPlaybackValueProvider.notifier).reset(); + } + } + + context.pushRoute( + DriftMemoryRoute( + memories: memories, + memoryIndex: index, + ), + ); + }, + children: memories.map((memory) => DriftMemoryCard(memory: memory)).toList(), + ), + ); + } +} + +class DriftMemoryCard extends ConsumerWidget { + const DriftMemoryCard({ + super.key, + required this.memory, + }); + + final DriftMemory memory; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final yearsAgo = DateTime.now().year - memory.data.year; + final title = 'years_ago'.t( + context: context, + args: { + 'years': yearsAgo.toString(), + }, + ); + return Center( + child: Stack( + children: [ + ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.black.withValues(alpha: 0.2), + BlendMode.darken, + ), + child: SizedBox( + width: 205, + height: 200, + child: Thumbnail( + remoteId: memory.assets[0].id, + fit: BoxFit.cover, + ), + ), + ), + Positioned( + bottom: 16, + left: 16, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 114, + ), + child: Text( + title, + style: const TextStyle( + fontWeight: FontWeight.w600, + color: Colors.white, + fontSize: 15, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/partner_user_avatar.widget.dart b/mobile/lib/presentation/widgets/partner_user_avatar.widget.dart new file mode 100644 index 0000000000..8cdf1ed286 --- /dev/null +++ b/mobile/lib/presentation/widgets/partner_user_avatar.widget.dart @@ -0,0 +1,31 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/services/api.service.dart'; + +class PartnerUserAvatar extends StatelessWidget { + const PartnerUserAvatar({super.key, required this.partner}); + + final PartnerUserDto partner; + + @override + Widget build(BuildContext context) { + final url = "${Store.get(StoreKey.serverEndpoint)}/users/${partner.id}/profile-image"; + final nameFirstLetter = partner.name.isNotEmpty ? partner.name[0] : ""; + return CircleAvatar( + radius: 16, + backgroundColor: context.primaryColor.withAlpha(50), + foregroundImage: CachedNetworkImageProvider( + url, + headers: ApiService.getRequestHeaders(), + cacheKey: "user-${partner.id}-profile", + ), + // silence errors if user has no profile image, use initials as fallback + onForegroundImageError: (exception, stackTrace) {}, + child: Text(nameFirstLetter.toUpperCase()), + ); + } +} diff --git a/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart b/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart new file mode 100644 index 0000000000..81989b263a --- /dev/null +++ b/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart @@ -0,0 +1,116 @@ +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'; + +class DriftRemoteAlbumOption extends ConsumerWidget { + const DriftRemoteAlbumOption({ + super.key, + this.onAddPhotos, + this.onAddUsers, + this.onDeleteAlbum, + this.onLeaveAlbum, + this.onCreateSharedLink, + this.onToggleAlbumOrder, + this.onEditAlbum, + }); + + final VoidCallback? onAddPhotos; + final VoidCallback? onAddUsers; + final VoidCallback? onDeleteAlbum; + final VoidCallback? onLeaveAlbum; + final VoidCallback? onCreateSharedLink; + final VoidCallback? onToggleAlbumOrder; + final VoidCallback? onEditAlbum; + + @override + Widget build(BuildContext context, WidgetRef ref) { + TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.w600, + ); + + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: ListView( + shrinkWrap: true, + children: [ + if (onEditAlbum != null) + ListTile( + leading: const Icon(Icons.edit), + title: Text( + 'edit_album'.t(context: context), + style: textStyle, + ), + onTap: onEditAlbum, + ), + if (onAddPhotos != null) + ListTile( + leading: const Icon(Icons.add_a_photo), + title: Text( + 'add_photos'.t(context: context), + style: textStyle, + ), + onTap: onAddPhotos, + ), + if (onAddUsers != null) + ListTile( + leading: const Icon(Icons.group_add), + title: Text( + 'album_viewer_page_share_add_users'.t(context: context), + style: textStyle, + ), + onTap: onAddUsers, + ), + if (onLeaveAlbum != null) + ListTile( + leading: const Icon(Icons.person_remove_rounded), + title: Text( + 'leave_album'.t(context: context), + style: textStyle, + ), + onTap: onLeaveAlbum, + ), + if (onToggleAlbumOrder != null) + ListTile( + leading: const Icon(Icons.swap_vert_rounded), + title: Text( + 'change_display_order'.t(context: context), + style: textStyle, + ), + onTap: onToggleAlbumOrder, + ), + if (onCreateSharedLink != null) + ListTile( + leading: const Icon(Icons.link), + title: Text( + 'create_shared_link'.t(context: context), + style: textStyle, + ), + onTap: onCreateSharedLink, + ), + if (onDeleteAlbum != null) ...[ + const Divider( + indent: 16, + endIndent: 16, + ), + ListTile( + leading: Icon( + Icons.delete, + color: context.isDarkTheme ? Colors.red[400] : Colors.red[800], + ), + title: Text( + 'delete_album'.t(context: context), + style: textStyle.copyWith( + color: context.isDarkTheme ? Colors.red[400] : Colors.red[800], + ), + ), + onTap: onDeleteAlbum, + ), + ], + ], + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/timeline/fixed/row.dart b/mobile/lib/presentation/widgets/timeline/fixed/row.dart index 1062c00740..24f3c97125 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/row.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/row.dart @@ -90,8 +90,7 @@ class RenderFixedRow extends RenderBox } } - double get intrinsicWidth => - dimension * childCount + spacing * (childCount - 1); + double get intrinsicWidth => dimension * childCount + spacing * (childCount - 1); @override double computeMinIntrinsicWidth(double height) => intrinsicWidth; diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart index bea754b3ff..88d113dcde 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart @@ -1,5 +1,6 @@ import 'dart:math' as math; +import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -9,7 +10,11 @@ import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; +import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; class FixedSegment extends Segment { final double tileHeight; @@ -33,100 +38,193 @@ class FixedSegment extends Segment { @override double indexToLayoutOffset(int index) { - index -= gridIndex; - if (index < 0) { - return startOffset; - } - return gridOffset + (mainAxisExtend * index); + final relativeIndex = index - gridIndex; + return relativeIndex < 0 ? startOffset : gridOffset + (mainAxisExtend * relativeIndex); } @override int getMinChildIndexForScrollOffset(double scrollOffset) { - scrollOffset -= gridOffset; - if (!scrollOffset.isFinite || scrollOffset < 0) { - return firstIndex; - } - final rowsAbove = (scrollOffset / mainAxisExtend).floor(); - return gridIndex + rowsAbove; + final adjustedOffset = scrollOffset - gridOffset; + if (!adjustedOffset.isFinite || adjustedOffset < 0) return firstIndex; + return gridIndex + (adjustedOffset / mainAxisExtend).floor(); } @override int getMaxChildIndexForScrollOffset(double scrollOffset) { - scrollOffset -= gridOffset; - if (!scrollOffset.isFinite || scrollOffset < 0) { - return firstIndex; - } - final firstRowBelow = (scrollOffset / mainAxisExtend).ceil(); - return gridIndex + firstRowBelow - 1; + final adjustedOffset = scrollOffset - gridOffset; + if (!adjustedOffset.isFinite || adjustedOffset < 0) return firstIndex; + return gridIndex + (adjustedOffset / mainAxisExtend).ceil() - 1; } @override Widget builder(BuildContext context, int index) { - if (index == firstIndex) { - return TimelineHeader( - bucket: bucket, - header: header, - height: headerExtent, - ); - } - final rowIndexInSegment = index - (firstIndex + 1); final assetIndex = rowIndexInSegment * columnCount; final assetCount = bucket.assetCount; final numberOfAssets = math.min(columnCount, assetCount - assetIndex); - return _buildRow(firstAssetIndex + assetIndex, numberOfAssets); + if (index == firstIndex) { + return TimelineHeader( + bucket: bucket, + header: header, + height: headerExtent, + assetOffset: firstAssetIndex, + ); + } + + return _FixedSegmentRow( + assetIndex: firstAssetIndex + assetIndex, + assetCount: numberOfAssets, + tileHeight: tileHeight, + spacing: spacing, + ); + } +} + +class _FixedSegmentRow extends ConsumerWidget { + final int assetIndex; + final int assetCount; + final double tileHeight; + final double spacing; + + const _FixedSegmentRow({ + required this.assetIndex, + required this.assetCount, + required this.tileHeight, + required this.spacing, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isScrubbing = ref.watch(timelineStateProvider.select((s) => s.isScrubbing)); + final timelineService = ref.read(timelineServiceProvider); + + if (isScrubbing) { + return _buildPlaceholder(context); + } + + if (timelineService.hasRange(assetIndex, assetCount)) { + return _buildAssetRow( + context, + timelineService.getAssets(assetIndex, assetCount), + ); + } + + return FutureBuilder>( + future: timelineService.loadAssets(assetIndex, assetCount), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return _buildPlaceholder(context); + } + return _buildAssetRow(context, snapshot.requireData); + }, + ); } - Widget _buildRow(int assetIndex, int count) => Consumer( - builder: (ctx, ref, _) { - final isScrubbing = - ref.watch(timelineStateProvider.select((s) => s.isScrubbing)); - final timelineService = ref.read(timelineServiceProvider); + Widget _buildPlaceholder(BuildContext context) { + return SegmentBuilder.buildPlaceholder( + context, + assetCount, + size: Size.square(tileHeight), + spacing: spacing, + ); + } - // Timeline is being scrubbed, show placeholders - if (isScrubbing) { - return SegmentBuilder.buildPlaceholder( - ctx, - count, - size: Size.square(tileHeight), - spacing: spacing, - ); - } + Widget _buildAssetRow(BuildContext context, List assets) { + return FixedTimelineRow( + dimension: tileHeight, + spacing: spacing, + textDirection: Directionality.of(context), + children: [ + for (int i = 0; i < assets.length; i++) + _AssetTileWidget( + key: ValueKey(assets[i].heroTag), + asset: assets[i], + assetIndex: assetIndex + i, + ), + ], + ); + } +} - // Bucket is already loaded, show the assets - if (timelineService.hasRange(assetIndex, count)) { - final assets = timelineService.getAssets(assetIndex, count); - return _buildAssetRow(ctx, assets); - } +class _AssetTileWidget extends ConsumerWidget { + final BaseAsset asset; + final int assetIndex; - // Bucket is not loaded, show placeholders and load the bucket - return FutureBuilder( - future: timelineService.loadAssets(assetIndex, count), - builder: (ctxx, snap) { - if (snap.connectionState != ConnectionState.done) { - return SegmentBuilder.buildPlaceholder( - ctx, - count, - size: Size.square(tileHeight), - spacing: spacing, - ); - } + const _AssetTileWidget({ + super.key, + required this.asset, + required this.assetIndex, + }); - return _buildAssetRow(ctxx, snap.requireData); - }, - ); - }, - ); + Future _handleOnTap( + BuildContext ctx, + WidgetRef ref, + int assetIndex, + BaseAsset asset, + int? heroOffset, + ) async { + final multiSelectState = ref.read(multiSelectProvider); - Widget _buildAssetRow(BuildContext context, List assets) => - FixedTimelineRow( - dimension: tileHeight, - spacing: spacing, - textDirection: Directionality.of(context), - children: List.generate( - assets.length, - (i) => RepaintBoundary(child: ThumbnailTile(assets[i])), + if (multiSelectState.forceEnable || multiSelectState.isEnabled) { + ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset); + } else { + await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1); + ref.read(isPlayingMotionVideoProvider.notifier).playing = false; + ctx.pushRoute( + AssetViewerRoute( + initialIndex: assetIndex, + timelineService: ref.read(timelineServiceProvider), + heroOffset: heroOffset, ), ); + } + } + + void _handleOnLongPress(WidgetRef ref, BaseAsset asset) { + final multiSelectState = ref.read(multiSelectProvider); + if (multiSelectState.isEnabled || multiSelectState.forceEnable) { + return; + } + + ref.read(hapticFeedbackProvider.notifier).heavyImpact(); + ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset); + } + + bool _getLockSelectionStatus(WidgetRef ref) { + final lockSelectionAssets = ref.read( + multiSelectProvider.select( + (state) => state.lockedSelectionAssets, + ), + ); + + if (lockSelectionAssets.isEmpty) { + return false; + } + + return lockSelectionAssets.contains(asset); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final heroOffset = TabsRouterScope.of(context)?.controller.activeIndex ?? 0; + + final lockSelection = _getLockSelectionStatus(ref); + final showStorageIndicator = ref.watch( + timelineArgsProvider.select((args) => args.showStorageIndicator), + ); + + return RepaintBoundary( + child: GestureDetector( + onTap: () => lockSelection ? null : _handleOnTap(context, ref, assetIndex, asset, heroOffset), + onLongPress: () => lockSelection ? null : _handleOnLongPress(ref, asset), + child: ThumbnailTile( + asset, + lockSelection: lockSelection, + showStorageIndicator: showStorageIndicator, + heroOffset: heroOffset, + ), + ), + ); + } } diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment_builder.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment_builder.dart index 327e690267..5e260a4e68 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment_builder.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment_builder.dart @@ -35,18 +35,15 @@ class FixedSegmentBuilder extends SegmentBuilder { final timelineHeader = switch (groupBy) { GroupAssetsBy.month => HeaderType.month, - GroupAssetsBy.day => - bucket is TimeBucket && bucket.date.month != previousDate?.month - ? HeaderType.monthAndDay - : HeaderType.day, + GroupAssetsBy.day || + GroupAssetsBy.auto => + bucket is TimeBucket && bucket.date.month != previousDate?.month ? HeaderType.monthAndDay : HeaderType.day, GroupAssetsBy.none => HeaderType.none, }; final headerExtent = SegmentBuilder.headerExtent(timelineHeader); final segmentStartOffset = startOffset; - startOffset += headerExtent + - (tileHeight * numberOfRows) + - spacing * (numberOfRows - 1); + startOffset += headerExtent + (tileHeight * numberOfRows) + spacing * (numberOfRows - 1); final segmentEndOffset = startOffset; segments.add( diff --git a/mobile/lib/presentation/widgets/timeline/header.widget.dart b/mobile/lib/presentation/widgets/timeline/header.widget.dart index f5cce1dbbb..da48acd7db 100644 --- a/mobile/lib/presentation/widgets/timeline/header.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/header.widget.dart @@ -1,18 +1,26 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; class TimelineHeader extends StatelessWidget { final Bucket bucket; final HeaderType header; final double height; + final int assetOffset; const TimelineHeader({ super.key, required this.bucket, required this.header, required this.height, + required this.assetOffset, }); String _formatMonth(BuildContext context, DateTime date) { @@ -34,27 +42,99 @@ class TimelineHeader extends StatelessWidget { } final date = (bucket as TimeBucket).date; - return Container( - padding: const EdgeInsets.only(left: 10, top: 30, bottom: 10), - height: height, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - if (header == HeaderType.month || header == HeaderType.monthAndDay) - Text( - _formatMonth(context, date), - style: context.textTheme.labelLarge - ?.copyWith(fontSize: 24, fontWeight: FontWeight.w500), - ), - if (header == HeaderType.day || header == HeaderType.monthAndDay) - Text( - _formatDay(context, date), - style: context.textTheme.labelLarge - ?.copyWith(fontWeight: FontWeight.w500), - ), - ], + + final isMonthHeader = header == HeaderType.month || header == HeaderType.monthAndDay; + final isDayHeader = header == HeaderType.day || header == HeaderType.monthAndDay; + + return Padding( + padding: EdgeInsets.only( + top: isMonthHeader ? 8.0 : 0.0, + left: 12.0, + right: 12.0, + ), + child: SizedBox( + height: height, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (isMonthHeader) + Row( + children: [ + Text( + _formatMonth(context, date), + style: context.textTheme.labelLarge?.copyWith(fontSize: 24), + ), + const Spacer(), + if (header != HeaderType.monthAndDay) + _BulkSelectIconButton( + bucket: bucket, + assetOffset: assetOffset, + ), + ], + ), + if (isDayHeader) + Row( + children: [ + Text( + _formatDay(context, date), + style: context.textTheme.labelLarge?.copyWith( + fontSize: 15, + ), + ), + const Spacer(), + _BulkSelectIconButton( + bucket: bucket, + assetOffset: assetOffset, + ), + ], + ), + ], + ), ), ); } } + +class _BulkSelectIconButton extends ConsumerWidget { + final Bucket bucket; + final int assetOffset; + + const _BulkSelectIconButton({ + required this.bucket, + required this.assetOffset, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + List bucketAssets; + try { + bucketAssets = ref.watch(timelineServiceProvider).getAssets(assetOffset, bucket.assetCount); + } catch (e) { + bucketAssets = []; + } + + final isAllSelected = ref.watch(bucketSelectionProvider(bucketAssets)); + + return IconButton( + onPressed: () { + ref.read(multiSelectProvider.notifier).toggleBucketSelection( + assetOffset, + bucket.assetCount, + ); + ref.read(hapticFeedbackProvider.notifier).heavyImpact(); + }, + icon: isAllSelected + ? Icon( + Icons.check_circle_rounded, + size: 26, + color: context.primaryColor, + ) + : Icon( + Icons.check_circle_outline_rounded, + size: 26, + color: context.colorScheme.onSurfaceSecondary, + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart index d68d9cfd67..6d23f169b7 100644 --- a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart @@ -13,7 +13,7 @@ import 'package:intl/intl.dart' hide TextDirection; /// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged /// for quick navigation of the BoxScrollView. -class Scrubber extends StatefulWidget { +class Scrubber extends ConsumerStatefulWidget { /// The view that will be scrolled with the scroll thumb final CustomScrollView child; @@ -26,6 +26,8 @@ class Scrubber extends StatefulWidget { final double bottomPadding; + final double? monthSegmentSnappingOffset; + Scrubber({ super.key, Key? scrollThumbKey, @@ -33,44 +35,55 @@ class Scrubber extends StatefulWidget { required this.timelineHeight, this.topPadding = 0, this.bottomPadding = 0, + this.monthSegmentSnappingOffset, required this.child, }) : assert(child.scrollDirection == Axis.vertical); @override - State createState() => ScrubberState(); + ConsumerState createState() => ScrubberState(); } List<_Segment> _buildSegments({ required List layoutSegments, required double timelineHeight, }) { + const double offsetThreshold = 20.0; + final segments = <_Segment>[]; if (layoutSegments.isEmpty || layoutSegments.first.bucket is! TimeBucket) { return []; } final formatter = DateFormat.yMMM(); + DateTime? lastDate; + double lastOffset = -offsetThreshold; for (final layoutSegment in layoutSegments) { - final scrollPercentage = - layoutSegment.startOffset / layoutSegments.last.endOffset; + final scrollPercentage = layoutSegment.startOffset / layoutSegments.last.endOffset; final startOffset = scrollPercentage * timelineHeight; final date = (layoutSegment.bucket as TimeBucket).date; final label = formatter.format(date); + final showSegment = lastOffset + offsetThreshold <= startOffset && (lastDate == null || date.year != lastDate.year); + segments.add( _Segment( date: date, startOffset: startOffset, scrollLabel: label, + showSegment: showSegment, ), ); + lastDate = date; + if (showSegment) { + lastOffset = startOffset; + } } return segments; } -class ScrubberState extends State with TickerProviderStateMixin { +class ScrubberState extends ConsumerState with TickerProviderStateMixin { double _thumbTopOffset = 0.0; bool _isDragging = false; List<_Segment> _segments = []; @@ -82,15 +95,15 @@ class ScrubberState extends State with TickerProviderStateMixin { late AnimationController _labelAnimationController; late Animation _labelAnimation; - double get _scrubberHeight => - widget.timelineHeight - widget.topPadding - widget.bottomPadding; + double get _scrubberHeight => widget.timelineHeight - widget.topPadding - widget.bottomPadding; - late final ScrollController _scrollController; + late ScrollController _scrollController; - double get _currentOffset => - _scrollController.offset * - _scrubberHeight / - _scrollController.position.maxScrollExtent; + double get _currentOffset { + if (_scrollController.hasClients != true) return 0.0; + + return _scrollController.offset * _scrubberHeight / _scrollController.position.maxScrollExtent; + } @override void initState() { @@ -129,8 +142,7 @@ class ScrubberState extends State with TickerProviderStateMixin { void didUpdateWidget(covariant Scrubber oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.layoutSegments.lastOrNull?.endOffset != - widget.layoutSegments.lastOrNull?.endOffset) { + if (oldWidget.layoutSegments.lastOrNull?.endOffset != widget.layoutSegments.lastOrNull?.endOffset) { _segments = _buildSegments( layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight, @@ -160,6 +172,12 @@ class ScrubberState extends State with TickerProviderStateMixin { return false; } + if (notification is ScrollStartNotification || notification is ScrollUpdateNotification) { + ref.read(timelineStateProvider.notifier).setScrolling(true); + } else if (notification is ScrollEndNotification) { + ref.read(timelineStateProvider.notifier).setScrolling(false); + } + setState(() { if (notification is ScrollUpdateNotification) { _thumbTopOffset = _currentOffset; @@ -176,7 +194,7 @@ class ScrubberState extends State with TickerProviderStateMixin { return false; } - void _onDragStart(WidgetRef ref) { + void _onDragStart(DragStartDetails _) { ref.read(timelineStateProvider.notifier).setScrubbing(true); setState(() { _isDragging = true; @@ -194,28 +212,103 @@ class ScrubberState extends State with TickerProviderStateMixin { _thumbAnimationController.forward(); } - final newOffset = - details.globalPosition.dy - widget.topPadding - widget.bottomPadding; + final dragPosition = _calculateDragPosition(details); + final nearestMonthSegment = _findNearestMonthSegment(dragPosition); + if (nearestMonthSegment != null) { + _snapToSegment(nearestMonthSegment); + } + } + + /// Calculate the drag position relative to the scrubber area + /// + /// This method converts the global drag coordinates from the gesture detector + /// into a position relative to the scrubber's active area (excluding padding). + /// + /// The scrubber has padding at the top and bottom, so we need to: + /// 1. Calculate the actual draggable area (timelineHeight - topPadding - bottomPadding) + /// 2. Convert the global Y position to a position within this draggable area + /// 3. Clamp the result to ensure it stays within bounds (0 to dragAreaHeight) + /// + /// Example: + /// - If timelineHeight = 800, topPadding = 50, bottomPadding = 50 + /// - Then dragAreaHeight = 700 (the actual scrubber area) + /// - If user drags to global Y position that's 100 pixels from the top + /// - The relative position would be 100 - 50 = 50 (50 pixels into the scrubber area) + double _calculateDragPosition(DragUpdateDetails details) { + final dragAreaTop = widget.topPadding; + final dragAreaBottom = widget.timelineHeight - widget.bottomPadding; + final dragAreaHeight = dragAreaBottom - dragAreaTop; + + final relativePosition = details.globalPosition.dy - dragAreaTop; + + // Make sure the position stays within the scrubber's bounds + return relativePosition.clamp(0.0, dragAreaHeight); + } + + /// Find the segment closest to the given position + _Segment? _findNearestMonthSegment(double position) { + _Segment? nearestSegment; + double minDistance = double.infinity; + + for (final segment in _segments) { + final distance = (segment.startOffset - position).abs(); + if (distance < minDistance) { + minDistance = distance; + nearestSegment = segment; + } + } + + return nearestSegment; + } + + /// Snap the scrubber thumb and scroll view to the given segment + void _snapToSegment(_Segment segment) { setState(() { - _thumbTopOffset = newOffset.clamp(0, _scrubberHeight); - final scrollPercentage = _thumbTopOffset / _scrubberHeight; - final maxScrollExtent = _scrollController.position.maxScrollExtent; - _scrollController.jumpTo(maxScrollExtent * scrollPercentage); + _thumbTopOffset = segment.startOffset; + + final layoutSegmentIndex = _findLayoutSegmentIndex(segment); + + if (layoutSegmentIndex >= 0) { + _scrollToLayoutSegment(layoutSegmentIndex); + } }); } - void _onDragEnd(WidgetRef ref) { + int _findLayoutSegmentIndex(_Segment segment) { + return widget.layoutSegments.indexWhere( + (layoutSegment) { + final bucket = layoutSegment.bucket as TimeBucket; + return bucket.date.year == segment.date.year && bucket.date.month == segment.date.month; + }, + ); + } + + void _scrollToLayoutSegment(int layoutSegmentIndex) { + final layoutSegment = widget.layoutSegments[layoutSegmentIndex]; + final maxScrollExtent = _scrollController.position.maxScrollExtent; + final viewportHeight = _scrollController.position.viewportDimension; + + final targetScrollOffset = layoutSegment.startOffset; + final centeredOffset = targetScrollOffset - (viewportHeight / 4) + 100 + (widget.monthSegmentSnappingOffset ?? 0.0); + + _scrollController.jumpTo(centeredOffset.clamp(0.0, maxScrollExtent)); + } + + void _onDragEnd(DragEndDetails _) { ref.read(timelineStateProvider.notifier).setScrubbing(false); _labelAnimationController.reverse(); - _isDragging = false; + setState(() { + _isDragging = false; + }); + _resetThumbTimer(); } @override Widget build(BuildContext ctx) { Text? label; - if (_scrollController.hasClients) { + if (_scrollController.hasClients == true) { // Cache to avoid multiple calls to [_currentOffset] final scrollOffset = _currentOffset; final labelText = _segments @@ -240,24 +333,99 @@ class ScrubberState extends State with TickerProviderStateMixin { child: Stack( children: [ RepaintBoundary(child: widget.child), - PositionedDirectional( - top: _thumbTopOffset + widget.topPadding, - end: 0, - child: Consumer( - builder: (_, ref, child) => GestureDetector( - onVerticalDragStart: (_) => _onDragStart(ref), - onVerticalDragUpdate: _onDragUpdate, - onVerticalDragEnd: (_) => _onDragEnd(ref), - child: child, + // Scroll Segments - wrapped in RepaintBoundary for better performance + RepaintBoundary( + child: _SegmentsLayer( + key: ValueKey('segments_${_isDragging}_${_segments.length}'), + segments: _segments, + topPadding: widget.topPadding, + isDragging: _isDragging, + ), + ), + if (_scrollController.hasClients && _scrollController.position.maxScrollExtent > 0) + PositionedDirectional( + top: _thumbTopOffset + widget.topPadding, + end: 0, + child: RepaintBoundary( + child: GestureDetector( + onVerticalDragStart: _onDragStart, + onVerticalDragUpdate: _onDragUpdate, + onVerticalDragEnd: _onDragEnd, + child: _Scrubber( + thumbAnimation: _thumbAnimation, + labelAnimation: _labelAnimation, + label: label, + ), + ), ), - child: _Scrubber( - thumbAnimation: _thumbAnimation, - labelAnimation: _labelAnimation, - label: label, + ), + ], + ), + ); + } +} + +class _SegmentsLayer extends StatelessWidget { + final List<_Segment> segments; + final double topPadding; + final bool isDragging; + + const _SegmentsLayer({ + super.key, + required this.segments, + required this.topPadding, + required this.isDragging, + }); + + @override + Widget build(BuildContext context) { + return Visibility( + visible: isDragging, + child: Stack( + children: segments + .where((segment) => segment.showSegment) + .map( + (segment) => PositionedDirectional( + key: ValueKey('segment_${segment.date.millisecondsSinceEpoch}'), + top: topPadding + segment.startOffset, + end: 100, + child: RepaintBoundary( + child: _SegmentWidget(segment), + ), + ), + ) + .toList(), + ), + ); + } +} + +class _SegmentWidget extends StatelessWidget { + final _Segment _segment; + + const _SegmentWidget(this._segment); + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: Container( + margin: const EdgeInsets.only(right: 12.0), + child: Material( + color: context.colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(16.0)), + child: Container( + constraints: const BoxConstraints(maxHeight: 28), + padding: const EdgeInsets.symmetric(horizontal: 10.0), + alignment: Alignment.center, + child: Text( + _segment.date.year.toString(), + style: context.textTheme.labelMedium?.copyWith( + fontFamily: "OverpassMono", + fontWeight: FontWeight.w600, ), ), ), - ], + ), ), ); } @@ -311,9 +479,8 @@ class _Scrubber extends StatelessWidget { @override Widget build(BuildContext context) { - final backgroundColor = context.isDarkTheme - ? context.colorScheme.primary.darken(amount: .5) - : context.colorScheme.primary; + final backgroundColor = + context.isDarkTheme ? context.colorScheme.primary.darken(amount: .5) : context.colorScheme.primary; return _SlideFadeTransition( animation: thumbAnimation, @@ -409,8 +576,7 @@ class _SlideFadeTransition extends StatelessWidget { Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, - builder: (context, child) => - _animation.value == 0.0 ? const SizedBox() : child!, + builder: (context, child) => _animation.value == 0.0 ? const SizedBox() : child!, child: SlideTransition( position: Tween( begin: const Offset(0.3, 0.0), @@ -429,22 +595,26 @@ class _Segment { final DateTime date; final double startOffset; final String scrollLabel; + final bool showSegment; const _Segment({ required this.date, required this.startOffset, required this.scrollLabel, + this.showSegment = false, }); _Segment copyWith({ DateTime? date, double? startOffset, String? scrollLabel, + bool? showSegment, }) { return _Segment( date: date ?? this.date, startOffset: startOffset ?? this.startOffset, scrollLabel: scrollLabel ?? this.scrollLabel, + showSegment: showSegment ?? this.showSegment, ); } diff --git a/mobile/lib/presentation/widgets/timeline/segment.model.dart b/mobile/lib/presentation/widgets/timeline/segment.model.dart index 09d892f69a..6a20db4c98 100644 --- a/mobile/lib/presentation/widgets/timeline/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/segment.model.dart @@ -42,8 +42,7 @@ abstract class Segment { bool containsIndex(int index) => firstIndex <= index && index <= lastIndex; - bool isWithinOffset(double offset) => - startOffset <= offset && offset <= endOffset; + bool isWithinOffset(double offset) => startOffset <= offset && offset <= endOffset; int getMinChildIndexForScrollOffset(double scrollOffset); int getMaxChildIndexForScrollOffset(double scrollOffset); @@ -88,13 +87,9 @@ abstract class Segment { } extension SegmentListExtension on List { - bool equals(List other) => - length == other.length && - lastOrNull?.endOffset == other.lastOrNull?.endOffset; + bool equals(List other) => length == other.length && lastOrNull?.endOffset == other.lastOrNull?.endOffset; - Segment? findByIndex(int index) => - firstWhereOrNull((s) => s.containsIndex(index)); + Segment? findByIndex(int index) => firstWhereOrNull((s) => s.containsIndex(index)); - Segment? findByOffset(double offset) => - firstWhereOrNull((s) => s.isWithinOffset(offset)) ?? lastOrNull; + Segment? findByOffset(double offset) => firstWhereOrNull((s) => s.isWithinOffset(offset)) ?? lastOrNull; } diff --git a/mobile/lib/presentation/widgets/timeline/segment_builder.dart b/mobile/lib/presentation/widgets/timeline/segment_builder.dart index 97031c623f..a746eab243 100644 --- a/mobile/lib/presentation/widgets/timeline/segment_builder.dart +++ b/mobile/lib/presentation/widgets/timeline/segment_builder.dart @@ -15,18 +15,12 @@ abstract class SegmentBuilder { this.groupBy = GroupAssetsBy.day, }); - static double headerExtent(HeaderType header) { - switch (header) { - case HeaderType.month: - return kTimelineHeaderExtent; - case HeaderType.day: - return kTimelineHeaderExtent * 0.90; - case HeaderType.monthAndDay: - return kTimelineHeaderExtent * 1.5; - case HeaderType.none: - return 0.0; - } - } + static double headerExtent(HeaderType header) => switch (header) { + HeaderType.month => kTimelineHeaderExtent, + HeaderType.day => kTimelineHeaderExtent * 0.90, + HeaderType.monthAndDay => kTimelineHeaderExtent * 1.6, + HeaderType.none => 0.0, + }; static Widget buildPlaceholder( BuildContext context, diff --git a/mobile/lib/presentation/widgets/timeline/timeline.state.dart b/mobile/lib/presentation/widgets/timeline/timeline.state.dart index 6e38bf2ac1..cdf79239d4 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.state.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.state.dart @@ -14,12 +14,18 @@ class TimelineArgs { final double maxHeight; final double spacing; final int columnCount; + final bool showStorageIndicator; + final bool withStack; + final GroupAssetsBy? groupBy; const TimelineArgs({ required this.maxWidth, required this.maxHeight, this.spacing = kTimelineSpacing, this.columnCount = kTimelineColumnCount, + this.showStorageIndicator = false, + this.withStack = false, + this.groupBy, }); @override @@ -27,7 +33,10 @@ class TimelineArgs { return spacing == other.spacing && maxWidth == other.maxWidth && maxHeight == other.maxHeight && - columnCount == other.columnCount; + columnCount == other.columnCount && + showStorageIndicator == other.showStorageIndicator && + withStack == other.withStack && + groupBy == other.groupBy; } @override @@ -35,24 +44,36 @@ class TimelineArgs { maxWidth.hashCode ^ maxHeight.hashCode ^ spacing.hashCode ^ - columnCount.hashCode; + columnCount.hashCode ^ + showStorageIndicator.hashCode ^ + withStack.hashCode ^ + groupBy.hashCode; } class TimelineState { final bool isScrubbing; + final bool isScrolling; - const TimelineState({this.isScrubbing = false}); + const TimelineState({ + this.isScrubbing = false, + this.isScrolling = false, + }); + + bool get isInteracting => isScrubbing || isScrolling; @override bool operator ==(covariant TimelineState other) { - return isScrubbing == other.isScrubbing; + return isScrubbing == other.isScrubbing && isScrolling == other.isScrolling; } @override - int get hashCode => isScrubbing.hashCode; + int get hashCode => isScrubbing.hashCode ^ isScrolling.hashCode; - TimelineState copyWith({bool? isScrubbing}) { - return TimelineState(isScrubbing: isScrubbing ?? this.isScrubbing); + TimelineState copyWith({bool? isScrubbing, bool? isScrolling}) { + return TimelineState( + isScrubbing: isScrubbing ?? this.isScrubbing, + isScrolling: isScrolling ?? this.isScrolling, + ); } } @@ -63,8 +84,15 @@ class TimelineStateNotifier extends Notifier { state = state.copyWith(isScrubbing: isScrubbing); } + void setScrolling(bool isScrolling) { + state = state.copyWith(isScrolling: isScrolling); + } + @override - TimelineState build() => const TimelineState(isScrubbing: false); + TimelineState build() => const TimelineState( + isScrubbing: false, + isScrolling: false, + ); } // This provider watches the buckets from the timeline service & args and serves the segments. @@ -77,8 +105,7 @@ final timelineSegmentProvider = StreamProvider.autoDispose>( final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1)); final tileExtent = math.max(0, availableTileWidth) / columnCount; - final groupBy = GroupAssetsBy - .values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)]; + final groupBy = args.groupBy ?? GroupAssetsBy.values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)]; final timelineService = ref.watch(timelineServiceProvider); yield* timelineService.watchBuckets().map((buckets) { @@ -94,7 +121,6 @@ final timelineSegmentProvider = StreamProvider.autoDispose>( dependencies: [timelineServiceProvider, timelineArgsProvider], ); -final timelineStateProvider = - NotifierProvider( +final timelineStateProvider = NotifierProvider( TimelineStateNotifier.new, ); diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 6ea3ddaf44..2439fd100b 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:collection/collection.dart'; @@ -6,20 +7,49 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; +import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart'; class Timeline extends StatelessWidget { - const Timeline({super.key}); + const Timeline({ + super.key, + this.topSliverWidget, + this.topSliverWidgetHeight, + this.showStorageIndicator = false, + this.withStack = false, + this.appBar = const ImmichSliverAppBar( + floating: true, + pinned: false, + snap: false, + ), + this.bottomSheet = const GeneralBottomSheet(), + this.groupBy, + }); + + final Widget? topSliverWidget; + final double? topSliverWidgetHeight; + final bool showStorageIndicator; + final Widget? appBar; + final Widget? bottomSheet; + final bool withStack; + final GroupAssetsBy? groupBy; @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: false, body: LayoutBuilder( builder: (_, constraints) => ProviderScope( overrides: [ @@ -30,62 +60,98 @@ class Timeline extends StatelessWidget { columnCount: ref.watch( settingsProvider.select((s) => s.get(Setting.tilesPerRow)), ), + showStorageIndicator: showStorageIndicator, + withStack: withStack, + groupBy: groupBy, ), ), ], - child: const _SliverTimeline(), + child: _SliverTimeline( + topSliverWidget: topSliverWidget, + topSliverWidgetHeight: topSliverWidgetHeight, + appBar: appBar, + bottomSheet: bottomSheet, + ), ), ), ); } } -class _SliverTimeline extends StatefulWidget { - const _SliverTimeline(); +class _SliverTimeline extends ConsumerStatefulWidget { + const _SliverTimeline({ + this.topSliverWidget, + this.topSliverWidgetHeight, + this.appBar, + this.bottomSheet, + }); + + final Widget? topSliverWidget; + final double? topSliverWidgetHeight; + final Widget? appBar; + final Widget? bottomSheet; @override - State createState() => _SliverTimelineState(); + ConsumerState createState() => _SliverTimelineState(); } -class _SliverTimelineState extends State<_SliverTimeline> { +class _SliverTimelineState extends ConsumerState<_SliverTimeline> { final _scrollController = ScrollController(); + StreamSubscription? _reloadSubscription; + + @override + void initState() { + super.initState(); + _reloadSubscription = EventStream.shared.listen((_) => setState(() {})); + } @override void dispose() { _scrollController.dispose(); + _reloadSubscription?.cancel(); super.dispose(); } @override Widget build(BuildContext _) { - return Consumer( - builder: (context, ref, child) { - final asyncSegments = ref.watch(timelineSegmentProvider); - final maxHeight = - ref.watch(timelineArgsProvider.select((args) => args.maxHeight)); - return asyncSegments.widgetWhen( - onData: (segments) { - final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1; + final asyncSegments = ref.watch(timelineSegmentProvider); + final maxHeight = ref.watch(timelineArgsProvider.select((args) => args.maxHeight)); + final isSelectionMode = ref.watch( + multiSelectProvider.select((s) => s.forceEnable), + ); - return PrimaryScrollController( - controller: _scrollController, - child: Scrubber( + return asyncSegments.widgetWhen( + onData: (segments) { + final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1; + final double appBarExpandedHeight = widget.appBar != null && widget.appBar is MesmerizingSliverAppBar ? 200 : 0; + final topPadding = context.padding.top + (widget.appBar == null ? 0 : kToolbarHeight) + 10; + + const scrubberBottomPadding = 100.0; + final bottomPadding = context.padding.bottom + (widget.appBar == null ? 0 : scrubberBottomPadding); + + return PrimaryScrollController( + controller: _scrollController, + child: Stack( + children: [ + Scrubber( layoutSegments: segments, timelineHeight: maxHeight, - topPadding: context.padding.top + 10, - bottomPadding: context.padding.bottom + 10, + topPadding: topPadding, + bottomPadding: bottomPadding, + monthSegmentSnappingOffset: widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight, child: CustomScrollView( primary: true, cacheExtent: maxHeight * 2, slivers: [ + if (isSelectionMode) const SelectionSliverAppBar() else if (widget.appBar != null) widget.appBar!, + if (widget.topSliverWidget != null) widget.topSliverWidget!, _SliverSegmentedList( segments: segments, delegate: SliverChildBuilderDelegate( (ctx, index) { if (index >= childCount) return null; final segment = segments.findByIndex(index); - return segment?.builder(ctx, index) ?? - const SizedBox.shrink(); + return segment?.builder(ctx, index) ?? const SizedBox.shrink(); }, childCount: childCount, addAutomaticKeepAlives: false, @@ -93,11 +159,53 @@ class _SliverTimelineState extends State<_SliverTimeline> { addRepaintBoundaries: false, ), ), + const SliverPadding( + padding: EdgeInsets.only( + bottom: scrubberBottomPadding, + ), + ), ], ), ), - ); - }, + if (!isSelectionMode) ...[ + Consumer( + builder: (_, consumerRef, child) { + final isMultiSelectEnabled = consumerRef.watch( + multiSelectProvider.select( + (s) => s.isEnabled, + ), + ); + + if (isMultiSelectEnabled) { + return child!; + } + return const SizedBox.shrink(); + }, + child: const Positioned( + top: 60, + left: 25, + child: _MultiSelectStatusButton(), + ), + ), + if (widget.bottomSheet != null) + Consumer( + builder: (_, consumerRef, child) { + final isMultiSelectEnabled = consumerRef.watch( + multiSelectProvider.select( + (s) => s.isEnabled, + ), + ); + + if (isMultiSelectEnabled) { + return child!; + } + return const SizedBox.shrink(); + }, + child: widget.bottomSheet, + ), + ], + ], + ), ); }, ); @@ -113,8 +221,7 @@ class _SliverSegmentedList extends SliverMultiBoxAdaptorWidget { }) : _segments = segments; @override - _RenderSliverTimelineBoxAdaptor createRenderObject(BuildContext context) => - _RenderSliverTimelineBoxAdaptor( + _RenderSliverTimelineBoxAdaptor createRenderObject(BuildContext context) => _RenderSliverTimelineBoxAdaptor( childManager: context as SliverMultiBoxAdaptorElement, segments: _segments, ); @@ -146,17 +253,13 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor { }) : _segments = segments; int getMinChildIndexForScrollOffset(double offset) => - _segments.findByOffset(offset)?.getMinChildIndexForScrollOffset(offset) ?? - 0; + _segments.findByOffset(offset)?.getMinChildIndexForScrollOffset(offset) ?? 0; int getMaxChildIndexForScrollOffset(double offset) => - _segments.findByOffset(offset)?.getMaxChildIndexForScrollOffset(offset) ?? - 0; + _segments.findByOffset(offset)?.getMaxChildIndexForScrollOffset(offset) ?? 0; double indexToLayoutOffset(int index) => - (_segments.findByIndex(index) ?? _segments.lastOrNull) - ?.indexToLayoutOffset(index) ?? - 0; + (_segments.findByIndex(index) ?? _segments.lastOrNull)?.indexToLayoutOffset(index) ?? 0; double estimateMaxScrollOffset() => _segments.lastOrNull?.endOffset ?? 0; @@ -168,8 +271,7 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor { // Assume initially that we have enough children to fill the viewport/cache area. childManager.setDidUnderflow(false); - final double scrollOffset = - constraints.scrollOffset + constraints.cacheOrigin; + final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin; assert(scrollOffset >= 0.0); final double remainingExtent = constraints.remainingCacheExtent; @@ -178,31 +280,26 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor { final double targetScrollOffset = scrollOffset + remainingExtent; // Find the index of the first child that should be visible or in the leading cache area. - final int firstRequiredChildIndex = - getMinChildIndexForScrollOffset(scrollOffset); + final int firstRequiredChildIndex = getMinChildIndexForScrollOffset(scrollOffset); // Find the index of the last child that should be visible or in the trailing cache area. - final int? lastRequiredChildIndex = targetScrollOffset.isFinite - ? getMaxChildIndexForScrollOffset(targetScrollOffset) - : null; + final int? lastRequiredChildIndex = + targetScrollOffset.isFinite ? getMaxChildIndexForScrollOffset(targetScrollOffset) : null; // Remove children that are no longer visible or within the cache area. if (firstChild == null) { collectGarbage(0, 0); } else { - final int leadingChildrenToRemove = - calculateLeadingGarbage(firstIndex: firstRequiredChildIndex); - final int trailingChildrenToRemove = lastRequiredChildIndex == null - ? 0 - : calculateTrailingGarbage(lastIndex: lastRequiredChildIndex); + final int leadingChildrenToRemove = calculateLeadingGarbage(firstIndex: firstRequiredChildIndex); + final int trailingChildrenToRemove = + lastRequiredChildIndex == null ? 0 : calculateTrailingGarbage(lastIndex: lastRequiredChildIndex); collectGarbage(leadingChildrenToRemove, trailingChildrenToRemove); } // If there are currently no children laid out (e.g., initial load), // try to add the first child needed for the current scroll offset. if (firstChild == null) { - final double firstChildLayoutOffset = - indexToLayoutOffset(firstRequiredChildIndex); + final double firstChildLayoutOffset = indexToLayoutOffset(firstRequiredChildIndex); final bool childAdded = addInitialChild( index: firstRequiredChildIndex, layoutOffset: firstChildLayoutOffset, @@ -210,8 +307,7 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor { if (!childAdded) { // There are either no children, or we are past the end of all our children. - final double max = - firstRequiredChildIndex <= 0 ? 0.0 : computeMaxScrollOffset(); + final double max = firstRequiredChildIndex <= 0 ? 0.0 : computeMaxScrollOffset(); geometry = SliverGeometry(scrollExtent: max, maxPaintExtent: max); childManager.didFinishLayout(); return; @@ -222,26 +318,20 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor { RenderBox? highestLaidOutChild; final childConstraints = constraints.asBoxConstraints(); - for (int currentIndex = indexOf(firstChild!) - 1; - currentIndex >= firstRequiredChildIndex; - --currentIndex) { - final RenderBox? newLeadingChild = - insertAndLayoutLeadingChild(childConstraints); + for (int currentIndex = indexOf(firstChild!) - 1; currentIndex >= firstRequiredChildIndex; --currentIndex) { + final RenderBox? newLeadingChild = insertAndLayoutLeadingChild(childConstraints); if (newLeadingChild == null) { // If a child is missing where we expect one, it indicates // an inconsistency in offset that needs correction. - final Segment? segment = - _segments.findByIndex(currentIndex) ?? _segments.firstOrNull; + final Segment? segment = _segments.findByIndex(currentIndex) ?? _segments.firstOrNull; geometry = SliverGeometry( // Request a scroll correction based on where the missing child should have been. - scrollOffsetCorrection: - segment?.indexToLayoutOffset(currentIndex) ?? 0.0, + scrollOffsetCorrection: segment?.indexToLayoutOffset(currentIndex) ?? 0.0, ); // Parent will re-layout everything. return; } - final childParentData = - newLeadingChild.parentData! as SliverMultiBoxAdaptorParentData; + final childParentData = newLeadingChild.parentData! as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = indexToLayoutOffset(currentIndex); assert(childParentData.index == currentIndex); highestLaidOutChild ??= newLeadingChild; @@ -255,10 +345,8 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor { // The [firstChild] that existed at the start of performLayout is still the first one we need. if (highestLaidOutChild == null) { firstChild!.layout(childConstraints); - final childParentData = - firstChild!.parentData! as SliverMultiBoxAdaptorParentData; - childParentData.layoutOffset = - indexToLayoutOffset(firstRequiredChildIndex); + final childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData; + childParentData.layoutOffset = indexToLayoutOffset(firstRequiredChildIndex); highestLaidOutChild = firstChild; } @@ -269,8 +357,7 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor { double calculatedMaxScrollOffset = double.infinity; for (int currentIndex = indexOf(mostRecentlyLaidOutChild!) + 1; - lastRequiredChildIndex == null || - currentIndex <= lastRequiredChildIndex; + lastRequiredChildIndex == null || currentIndex <= lastRequiredChildIndex; ++currentIndex) { RenderBox? child = childAfter(mostRecentlyLaidOutChild!); @@ -280,11 +367,8 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor { after: mostRecentlyLaidOutChild, ); if (child == null) { - final Segment? segment = - _segments.findByIndex(currentIndex) ?? _segments.lastOrNull; - calculatedMaxScrollOffset = - segment?.indexToLayoutOffset(currentIndex) ?? - computeMaxScrollOffset(); + final Segment? segment = _segments.findByIndex(currentIndex) ?? _segments.lastOrNull; + calculatedMaxScrollOffset = segment?.indexToLayoutOffset(currentIndex) ?? computeMaxScrollOffset(); break; } } else { @@ -292,28 +376,23 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor { } mostRecentlyLaidOutChild = child; - final childParentData = mostRecentlyLaidOutChild.parentData! - as SliverMultiBoxAdaptorParentData; + final childParentData = mostRecentlyLaidOutChild.parentData! as SliverMultiBoxAdaptorParentData; assert(childParentData.index == currentIndex); childParentData.layoutOffset = indexToLayoutOffset(currentIndex); } final int lastLaidOutChildIndex = indexOf(lastChild!); - final double leadingScrollOffset = - indexToLayoutOffset(firstRequiredChildIndex); - final double trailingScrollOffset = - indexToLayoutOffset(lastLaidOutChildIndex + 1); + final double leadingScrollOffset = indexToLayoutOffset(firstRequiredChildIndex); + final double trailingScrollOffset = indexToLayoutOffset(lastLaidOutChildIndex + 1); assert( firstRequiredChildIndex == 0 || - (childScrollOffset(firstChild!) ?? -1.0) - scrollOffset <= - precisionErrorTolerance, + (childScrollOffset(firstChild!) ?? -1.0) - scrollOffset <= precisionErrorTolerance, ); assert(debugAssertChildListIsNonEmptyAndContiguous()); assert(indexOf(firstChild!) == firstRequiredChildIndex); assert( - lastRequiredChildIndex == null || - lastLaidOutChildIndex <= lastRequiredChildIndex, + lastRequiredChildIndex == null || lastLaidOutChildIndex <= lastRequiredChildIndex, ); calculatedMaxScrollOffset = math.min( @@ -333,11 +412,9 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor { to: trailingScrollOffset, ); - final double targetEndScrollOffsetForPaint = - constraints.scrollOffset + constraints.remainingPaintExtent; - final int? targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite - ? getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint) - : null; + final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent; + final int? targetLastIndexForPaint = + targetEndScrollOffsetForPaint.isFinite ? getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint) : null; final maxPaintExtent = math.max(paintExtent, calculatedMaxScrollOffset); @@ -348,8 +425,7 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor { // Indicates if there's content scrolled off-screen. // This is true if the last child needed for painting is actually laid out, // or if the first child is partially visible. - hasVisualOverflow: (targetLastIndexForPaint != null && - lastLaidOutChildIndex >= targetLastIndexForPaint) || + hasVisualOverflow: (targetLastIndexForPaint != null && lastLaidOutChildIndex >= targetLastIndexForPaint) || constraints.scrollOffset > 0.0, cacheExtent: cacheExtent, ); @@ -363,3 +439,26 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor { childManager.didFinishLayout(); } } + +class _MultiSelectStatusButton extends ConsumerWidget { + const _MultiSelectStatusButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectCount = ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length)); + return ElevatedButton.icon( + onPressed: () => ref.read(multiSelectProvider.notifier).reset(), + icon: Icon( + Icons.close_rounded, + color: context.colorScheme.onPrimary, + ), + label: Text( + selectCount.toString(), + style: context.textTheme.titleMedium?.copyWith( + height: 2.5, + color: context.colorScheme.onPrimary, + ), + ), + ); + } +} diff --git a/mobile/lib/providers/activity.provider.dart b/mobile/lib/providers/activity.provider.dart index 0dcc99320b..38b8caaed9 100644 --- a/mobile/lib/providers/activity.provider.dart +++ b/mobile/lib/providers/activity.provider.dart @@ -11,9 +11,7 @@ part 'activity.provider.g.dart'; class AlbumActivity extends _$AlbumActivity { @override Future> build(String albumId, [String? assetId]) async { - return ref - .watch(activityServiceProvider) - .getAllActivities(albumId, assetId: assetId); + return ref.watch(activityServiceProvider).getAllActivities(albumId, assetId: assetId); } Future removeActivity(String id) async { @@ -24,17 +22,13 @@ class AlbumActivity extends _$AlbumActivity { state = AsyncData(activities); // Decrement activity count only for comments if (removedActivity.type == ActivityType.comment) { - ref - .watch(activityStatisticsProvider(albumId, assetId).notifier) - .removeActivity(); + ref.watch(activityStatisticsProvider(albumId, assetId).notifier).removeActivity(); } } } Future addLike() async { - final activity = await ref - .watch(activityServiceProvider) - .addActivity(albumId, ActivityType.like, assetId: assetId); + final activity = await ref.watch(activityServiceProvider).addActivity(albumId, ActivityType.like, assetId: assetId); if (activity.hasValue) { final activities = state.asData?.value ?? []; state = AsyncData([...activities, activity.requireValue]); @@ -52,9 +46,7 @@ class AlbumActivity extends _$AlbumActivity { if (activity.hasValue) { final activities = state.valueOrNull ?? []; state = AsyncData([...activities, activity.requireValue]); - ref - .watch(activityStatisticsProvider(albumId, assetId).notifier) - .addActivity(); + 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) { diff --git a/mobile/lib/providers/activity_service.provider.dart b/mobile/lib/providers/activity_service.provider.dart index 2d63e55354..a7fd0715f8 100644 --- a/mobile/lib/providers/activity_service.provider.dart +++ b/mobile/lib/providers/activity_service.provider.dart @@ -6,5 +6,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'activity_service.provider.g.dart'; @riverpod -ActivityService activityService(Ref ref) => - ActivityService(ref.watch(activityApiRepositoryProvider)); +ActivityService activityService(Ref ref) => ActivityService(ref.watch(activityApiRepositoryProvider)); diff --git a/mobile/lib/providers/activity_statistics.provider.dart b/mobile/lib/providers/activity_statistics.provider.dart index c260a7a547..96d2633d1b 100644 --- a/mobile/lib/providers/activity_statistics.provider.dart +++ b/mobile/lib/providers/activity_statistics.provider.dart @@ -9,10 +9,7 @@ part 'activity_statistics.provider.g.dart'; class ActivityStatistics extends _$ActivityStatistics { @override int build(String albumId, [String? assetId]) { - ref - .watch(activityServiceProvider) - .getStatistics(albumId, assetId: assetId) - .then((stats) => state = stats.comments); + ref.watch(activityServiceProvider).getStatistics(albumId, assetId: assetId).then((stats) => state = stats.comments); return 0; } diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart index 39f5af7344..ae565d20d8 100644 --- a/mobile/lib/providers/album/album.provider.dart +++ b/mobile/lib/providers/album/album.provider.dart @@ -18,8 +18,7 @@ class AlbumNotifier extends StateNotifier> { } }); - _streamSub = - albumService.watchRemoteAlbums().listen((data) => state = data); + _streamSub = albumService.watchRemoteAlbums().listen((data) => state = data); } final AlbumService albumService; @@ -114,8 +113,7 @@ class AlbumNotifier extends StateNotifier> { } Future toggleSortOrder(Album album) { - final order = - album.sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc; + final order = album.sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc; return albumService.updateSortOrder(album, order); } @@ -127,16 +125,14 @@ class AlbumNotifier extends StateNotifier> { } } -final albumProvider = - StateNotifierProvider.autoDispose>((ref) { +final albumProvider = StateNotifierProvider.autoDispose>((ref) { return AlbumNotifier( ref.watch(albumServiceProvider), ref, ); }); -final albumWatcher = - StreamProvider.autoDispose.family((ref, id) async* { +final albumWatcher = StreamProvider.autoDispose.family((ref, id) async* { final albumService = ref.watch(albumServiceProvider); final album = await albumService.getAlbumById(id); @@ -172,7 +168,6 @@ class LocalAlbumsNotifier extends StateNotifier> { } } -final localAlbumsProvider = - StateNotifierProvider.autoDispose>((ref) { +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 c89cd43132..6e1669faef 100644 --- a/mobile/lib/providers/album/album_sort_by_options.provider.dart +++ b/mobile/lib/providers/album/album_sort_by_options.provider.dart @@ -31,8 +31,7 @@ class _AlbumSortHandlers { static const AlbumSortFn assetCount = _sortByAssetCount; static List _sortByAssetCount(List albums, bool isReverse) { - final sorted = - albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount)); + final sorted = albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount)); return (isReverse ? sorted.reversed : sorted).toList(); } @@ -104,9 +103,7 @@ enum AlbumSortMode { class AlbumSortByOptions extends _$AlbumSortByOptions { @override AlbumSortMode build() { - final sortOpt = ref - .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.selectedAlbumSortOrder); + final sortOpt = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortOrder); return AlbumSortMode.values.firstWhere( (e) => e.storeIndex == sortOpt, orElse: () => AlbumSortMode.title, @@ -126,15 +123,11 @@ class AlbumSortByOptions extends _$AlbumSortByOptions { class AlbumSortOrder extends _$AlbumSortOrder { @override bool build() { - return ref - .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.selectedAlbumSortReverse); + return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortReverse); } void changeSortDirection(bool isReverse) { state = isReverse; - ref - .watch(appSettingsServiceProvider) - .setSetting(AppSettingsEnum.selectedAlbumSortReverse, isReverse); + ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortReverse, isReverse); } } diff --git a/mobile/lib/providers/album/album_viewer.provider.dart b/mobile/lib/providers/album/album_viewer.provider.dart index cf7344d321..afae154cd7 100644 --- a/mobile/lib/providers/album/album_viewer.provider.dart +++ b/mobile/lib/providers/album/album_viewer.provider.dart @@ -1,12 +1,12 @@ 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'; -import 'package:immich_mobile/entities/album.entity.dart'; class AlbumViewerNotifier extends StateNotifier { AlbumViewerNotifier(this.ref) : super( - AlbumViewerPageState( + const AlbumViewerPageState( editTitleText: "", isEditAlbum: false, editDescriptionText: "", @@ -88,7 +88,6 @@ class AlbumViewerNotifier extends StateNotifier { } } -final albumViewerProvider = - StateNotifierProvider((ref) { +final albumViewerProvider = StateNotifierProvider((ref) { return AlbumViewerNotifier(ref); }); diff --git a/mobile/lib/providers/album/suggested_shared_users.provider.dart b/mobile/lib/providers/album/suggested_shared_users.provider.dart index 3c8dcb6733..51146748c7 100644 --- a/mobile/lib/providers/album/suggested_shared_users.provider.dart +++ b/mobile/lib/providers/album/suggested_shared_users.provider.dart @@ -4,8 +4,7 @@ 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 { +final otherUsersProvider = FutureProvider.autoDispose>((ref) async { UserService userService = ref.watch(userServiceProvider); final currentUser = ref.watch(currentUserProvider); diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 3ec7813e2f..07778ed2ed 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -3,11 +3,15 @@ import 'dart:async'; import 'package:flutter/foundation.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/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'; @@ -15,9 +19,12 @@ import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:isar/isar.dart'; +import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; enum AppLifeCycleEnum { @@ -51,50 +58,83 @@ class AppLifeCycleNotifier extends StateNotifier { // Needs to be logged in if (isAuthenticated) { // switch endpoint if needed - final endpoint = - await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint(); + final endpoint = await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint(); if (kDebugMode) { debugPrint("Using server URL: $endpoint"); } - final permission = _ref.watch(galleryPermissionNotifier); - if (permission.isGranted || permission.isLimited) { - await _ref.read(backupProvider.notifier).resumeBackup(); - await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + 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(); } - switch (_ref.read(tabProvider)) { - case TabEnum.home: - await _ref.read(assetProvider.notifier).getAllAsset(); - break; - case TabEnum.search: - // nothing to do - break; + 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(); - break; - case TabEnum.library: - // nothing to do - break; + case TabEnum.albums: + await _ref.read(albumProvider.notifier).refreshRemoteAlbums(); + + case TabEnum.library: + case TabEnum.search: + break; + } + } else { + _ref.read(backupProvider.notifier).cancelBackup(); + + final backgroundManager = _ref.read(backgroundSyncProvider); + // Ensure proper cleanup before starting new background tasks + try { + await Future.wait([ + backgroundManager.syncLocal().then( + (_) { + Logger("AppLifeCycleNotifier").fine("Hashing assets after syncLocal"); + // Check if app is still active before hashing + if (state == AppLifeCycleEnum.resumed) { + backgroundManager.hashAssets(); + } + }, + ), + backgroundManager.syncRemote(), + ]).then((_) async { + final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); + + if (isEnableBackup) { + final currentUser = _ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + await _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); + } + }); + } catch (e, stackTrace) { + Logger("AppLifeCycleNotifier").severe( + "Error during background sync", + e, + stackTrace, + ); + } } _ref.read(websocketProvider.notifier).connect(); - await _ref - .read(notificationPermissionProvider.notifier) - .getNotificationPermission(); + await _ref.read(notificationPermissionProvider.notifier).getNotificationPermission(); - await _ref - .read(galleryPermissionNotifier.notifier) - .getGalleryPermissionStatus(); + await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus(); - await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); + if (!Store.isBetaTimelineEnabled) { + await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); - _ref.invalidate(memoryFutureProvider); + _ref.invalidate(memoryFutureProvider); + } } void handleAppInactivity() { @@ -107,23 +147,53 @@ class AppLifeCycleNotifier extends StateNotifier { _wasPaused = true; if (_ref.read(authProvider).isAuthenticated) { - // Do not cancel backup if manual upload is in progress - if (_ref.read(backupProvider.notifier).backupProgress != - BackUpProgressEnum.manualInProgress) { - _ref.read(backupProvider.notifier).cancelBackup(); + 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(); + } } + _ref.read(websocketProvider.notifier).disconnect(); } - LogService.I.flush(); + try { + LogService.I.flush(); + } catch (e) { + // Ignore flush errors during pause + } } Future handleAppDetached() async { state = AppLifeCycleEnum.detached; - LogService.I.flush(); - await Isar.getInstance()?.close(); + + // Flush logs before closing database + try { + LogService.I.flush(); + } catch (e) { + // Ignore flush errors during shutdown + } + + // Close Isar database safely + try { + final isar = Isar.getInstance(); + if (isar != null && isar.isOpen) { + await isar.close(); + } + } catch (e) { + // Ignore close errors during shutdown + } + + if (Store.isBetaTimelineEnabled) { + return; + } + // no guarantee this is called at all - _ref.read(manualUploadProvider.notifier).cancelBackup(); + try { + _ref.read(manualUploadProvider.notifier).cancelBackup(); + } catch (e) { + // Ignore errors during shutdown + } } void handleAppHidden() { @@ -132,7 +202,6 @@ class AppLifeCycleNotifier extends StateNotifier { } } -final appStateProvider = - StateNotifierProvider((ref) { +final appStateProvider = StateNotifierProvider((ref) { return AppLifeCycleNotifier(ref); }); diff --git a/mobile/lib/providers/app_settings.provider.dart b/mobile/lib/providers/app_settings.provider.dart index 2d4bdd0eef..109218a07c 100644 --- a/mobile/lib/providers/app_settings.provider.dart +++ b/mobile/lib/providers/app_settings.provider.dart @@ -5,4 +5,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'app_settings.provider.g.dart'; @Riverpod(keepAlive: true) -AppSettingsService appSettingsService(Ref _) => AppSettingsService(); +AppSettingsService appSettingsService(Ref _) => const AppSettingsService(); diff --git a/mobile/lib/providers/app_settings.provider.g.dart b/mobile/lib/providers/app_settings.provider.g.dart index 66814abd49..c959861c04 100644 --- a/mobile/lib/providers/app_settings.provider.g.dart +++ b/mobile/lib/providers/app_settings.provider.g.dart @@ -7,7 +7,7 @@ part of 'app_settings.provider.dart'; // ************************************************************************** String _$appSettingsServiceHash() => - r'2aa16d76a8df869c39486325efc1d08b2d2c284c'; + r'89cece3a19e06612f5639ae290120e854a0c5a31'; /// See also [appSettingsService]. @ProviderFor(appSettingsService) diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart index 5b77da90f3..5098dd2af4 100644 --- a/mobile/lib/providers/asset.provider.dart +++ b/mobile/lib/providers/asset.provider.dart @@ -81,7 +81,9 @@ class AssetNotifier extends StateNotifier { await _albumService.refreshDeviceAlbums(); } finally { _getAllAssetInProgress = false; - state = false; + if (mounted) { + state = false; + } } } @@ -180,8 +182,7 @@ class AssetNotifier extends StateNotifier { } } -final assetDetailProvider = - StreamProvider.autoDispose.family((ref, asset) async* { +final assetDetailProvider = StreamProvider.autoDispose.family((ref, asset) async* { final assetService = ref.watch(assetServiceProvider); yield await assetService.loadExif(asset); @@ -192,8 +193,7 @@ final assetDetailProvider = } }); -final assetWatcher = - StreamProvider.autoDispose.family((ref, 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 index b334ef193a..e2227920c7 100644 --- a/mobile/lib/providers/asset_viewer/asset_people.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_people.provider.dart @@ -17,9 +17,7 @@ class AssetPeopleNotifier extends _$AssetPeopleNotifier { return []; } - final list = await ref - .watch(assetServiceProvider) - .getRemotePeopleOfAsset(asset.remoteId!); + final list = await ref.watch(assetServiceProvider).getRemotePeopleOfAsset(asset.remoteId!); if (list == null) { return []; } diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart index 9bbbfb49aa..8772e3d0cb 100644 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart @@ -32,10 +32,8 @@ class AssetStackNotifier extends StateNotifier> { } } -final assetStackStateProvider = StateNotifierProvider.autoDispose - .family, String>( - (ref, stackId) => - AssetStackNotifier(ref.watch(assetServiceProvider), stackId), +final assetStackStateProvider = StateNotifierProvider.autoDispose.family, String>( + (ref, stackId) => AssetStackNotifier(ref.watch(assetServiceProvider), stackId), ); @riverpod diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart index 3daed6f686..b35a4546bb 100644 --- a/mobile/lib/providers/asset_viewer/download.provider.dart +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -23,7 +23,7 @@ class DownloadStateNotifier extends StateNotifier { this._shareService, this._albumService, ) : super( - DownloadState( + const DownloadState( downloadStatus: TaskStatus.complete, showProgress: false, taskProgress: {}, @@ -62,8 +62,7 @@ class DownloadStateNotifier extends StateNotifier { if (update.task.metaData.isEmpty) { return; } - final livePhotosId = - LivePhotosMetadata.fromJson(update.task.metaData).id; + final livePhotosId = LivePhotosMetadata.fromJson(update.task.metaData).id; _downloadService.saveLivePhotos(update.task, livePhotosId); _onDownloadComplete(update.task.taskId); break; @@ -191,8 +190,7 @@ class DownloadStateNotifier extends StateNotifier { } } -final downloadStateProvider = - StateNotifierProvider( +final downloadStateProvider = StateNotifierProvider( ((ref) => DownloadStateNotifier( ref.watch(downloadServiceProvider), ref.watch(shareServiceProvider), diff --git a/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart b/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart index 4af061f954..08722dc896 100644 --- a/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart +++ b/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart @@ -1,8 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; /// Whether to display the video part of a motion photo -final isPlayingMotionVideoProvider = - StateNotifierProvider((ref) { +final isPlayingMotionVideoProvider = StateNotifierProvider((ref) { return IsPlayingMotionVideo(ref); }); diff --git a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart b/mobile/lib/providers/asset_viewer/render_list_status_provider.dart index 903007031e..189ac85452 100644 --- a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart +++ b/mobile/lib/providers/asset_viewer/render_list_status_provider.dart @@ -2,8 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; enum RenderListStatusEnum { complete, empty, error, loading } -final renderListStatusProvider = - StateNotifierProvider((ref) { +final renderListStatusProvider = StateNotifierProvider((ref) { return RenderListStatus(ref); }); diff --git a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart index ed2c485b13..97730b5935 100644 --- a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart +++ b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart @@ -1,17 +1,19 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; -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/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/share_intent_service.dart'; import 'package:immich_mobile/services/upload.service.dart'; +import 'package:path/path.dart'; -final shareIntentUploadProvider = StateNotifierProvider< - ShareIntentUploadStateNotifier, List>( +final shareIntentUploadProvider = StateNotifierProvider>( ((ref) => ShareIntentUploadStateNotifier( ref.watch(appRouterProvider), ref.watch(uploadServiceProvider), @@ -19,8 +21,7 @@ final shareIntentUploadProvider = StateNotifierProvider< )), ); -class ShareIntentUploadStateNotifier - extends StateNotifier> { +class ShareIntentUploadStateNotifier extends StateNotifier> { final AppRouter router; final UploadService _uploadService; final ShareIntentService _shareIntentService; @@ -30,8 +31,8 @@ class ShareIntentUploadStateNotifier this._uploadService, this._shareIntentService, ) : super([]) { - _uploadService.onUploadStatus = _uploadStatusCallback; - _uploadService.onTaskProgress = _taskProgressCallback; + _uploadService.taskStatusStream.listen(_updateUploadStatus); + _uploadService.taskProgressStream.listen(_taskProgressCallback); } void init() { @@ -54,8 +55,7 @@ class ShareIntentUploadStateNotifier } void removeAttachment(ShareIntentAttachment attachment) { - final updatedState = - state.where((element) => element != attachment).toList(); + final updatedState = state.where((element) => element != attachment).toList(); if (updatedState.length != state.length) { state = updatedState; } @@ -69,8 +69,8 @@ class ShareIntentUploadStateNotifier state = []; } - void _updateUploadStatus(TaskStatusUpdate task, TaskStatus status) async { - if (status == TaskStatus.canceled) { + void _updateUploadStatus(TaskStatusUpdate task) async { + if (task.status == TaskStatus.canceled) { return; } @@ -83,61 +83,75 @@ class ShareIntentUploadStateNotifier TaskStatus.running => UploadStatus.running, TaskStatus.paused => UploadStatus.paused, TaskStatus.notFound => UploadStatus.notFound, - TaskStatus.waitingToRetry => UploadStatus.waitingtoRetry + TaskStatus.waitingToRetry => UploadStatus.waitingToRetry }; state = [ for (final attachment in state) - if (attachment.id == taskId.toInt()) - attachment.copyWith(status: uploadStatus) - else - attachment, + if (attachment.id == taskId.toInt()) attachment.copyWith(status: uploadStatus) else attachment, ]; } - void _uploadStatusCallback(TaskStatusUpdate update) { - _updateUploadStatus(update, update.status); - - switch (update.status) { - case TaskStatus.complete: - if (update.responseStatusCode == 200) { - if (kDebugMode) { - debugPrint("[COMPLETE] ${update.task.taskId} - DUPLICATE"); - } - } else { - if (kDebugMode) { - debugPrint("[COMPLETE] ${update.task.taskId}"); - } - } - break; - - default: - break; - } - } - void _taskProgressCallback(TaskProgressUpdate update) { // Ignore if the task is canceled or completed - if (update.progress == downloadFailed || - update.progress == downloadCompleted) { + if (update.progress == downloadFailed || update.progress == downloadCompleted) { return; } final taskId = update.task.taskId; state = [ for (final attachment in state) - if (attachment.id == taskId.toInt()) - attachment.copyWith(uploadProgress: update.progress) - else - attachment, + if (attachment.id == taskId.toInt()) attachment.copyWith(uploadProgress: update.progress) else attachment, ]; } - Future upload(File file) { - return _uploadService.upload(file); + Future upload(File file) async { + final task = await _buildUploadTask( + hash(file.path).toString(), + file, + ); + + _uploadService.enqueueTasks([task]); } - Future cancelUpload(String id) { - return _uploadService.cancelUpload(id); + Future _buildUploadTask( + String id, + File file, { + Map? fields, + }) async { + final serverEndpoint = Store.get(StoreKey.serverEndpoint); + final url = Uri.parse('$serverEndpoint/assets').toString(); + final headers = ApiService.getRequestHeaders(); + final deviceId = Store.get(StoreKey.deviceId); + + final (baseDirectory, directory, filename) = await Task.split(filePath: file.path); + final stats = await file.stat(); + final fileCreatedAt = stats.changed; + final fileModifiedAt = stats.modified; + + final fieldsMap = { + 'filename': filename, + 'deviceAssetId': id, + 'deviceId': deviceId, + 'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(), + 'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(), + 'isFavorite': 'false', + 'duration': '0', + if (fields != null) ...fields, + }; + + return UploadTask( + taskId: id, + httpRequestMethod: 'POST', + url: url, + headers: headers, + filename: filename, + fields: fieldsMap, + baseDirectory: baseDirectory, + directory: directory, + fileField: 'assetData', + group: kManualUploadGroup, + updates: Updates.statusAndProgress, + ); } } diff --git a/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart b/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart index 69be91480f..a758d97ad8 100644 --- a/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart +++ b/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart @@ -13,13 +13,11 @@ class VideoPlaybackControls { final bool restarted; } -final videoPlayerControlsProvider = - StateNotifierProvider((ref) { +final videoPlayerControlsProvider = StateNotifierProvider((ref) { return VideoPlayerControls(ref); }); -const videoPlayerControlsDefault = - VideoPlaybackControls(position: 0, pause: false); +const videoPlayerControlsDefault = VideoPlaybackControls(position: 0, pause: false); class VideoPlayerControls extends StateNotifier { VideoPlayerControls(this.ref) : super(videoPlayerControlsDefault); @@ -64,17 +62,14 @@ class VideoPlayerControls extends StateNotifier { } void togglePlay() { - state = - VideoPlaybackControls(position: state.position, pause: !state.pause); + state = VideoPlaybackControls(position: state.position, pause: !state.pause); } void restart() { - state = - const VideoPlaybackControls(position: 0, pause: false, restarted: true); - ref.read(videoPlaybackValueProvider.notifier).value = - ref.read(videoPlaybackValueProvider.notifier).value.copyWith( - state: VideoPlaybackState.playing, - position: Duration.zero, - ); + state = const VideoPlaybackControls(position: 0, pause: false, restarted: true); + ref.read(videoPlaybackValueProvider.notifier).value = ref.read(videoPlaybackValueProvider.notifier).value.copyWith( + state: VideoPlaybackState.playing, + position: Duration.zero, + ); } } diff --git a/mobile/lib/providers/asset_viewer/video_player_value_provider.dart b/mobile/lib/providers/asset_viewer/video_player_value_provider.dart index 1a3c54e9e9..55a381b02b 100644 --- a/mobile/lib/providers/asset_viewer/video_player_value_provider.dart +++ b/mobile/lib/providers/asset_viewer/video_player_value_provider.dart @@ -75,8 +75,7 @@ const VideoPlaybackValue videoPlaybackValueDefault = VideoPlaybackValue( volume: 0.0, ); -final videoPlaybackValueProvider = - StateNotifierProvider((ref) { +final videoPlaybackValueProvider = StateNotifierProvider((ref) { return VideoPlaybackValueState(ref); }); diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index dfbd18953a..b918eeb215 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -13,6 +13,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; +import 'package:immich_mobile/services/upload.service.dart'; import 'package:immich_mobile/services/widget.service.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; @@ -23,6 +24,7 @@ final authProvider = StateNotifierProvider((ref) { ref.watch(authServiceProvider), ref.watch(apiServiceProvider), ref.watch(userServiceProvider), + ref.watch(uploadServiceProvider), ref.watch(secureStorageServiceProvider), ref.watch(widgetServiceProvider), ); @@ -32,6 +34,7 @@ class AuthNotifier extends StateNotifier { final AuthService _authService; final ApiService _apiService; final UserService _userService; + final UploadService _uploadService; final SecureStorageService _secureStorageService; final WidgetService _widgetService; final _log = Logger("AuthenticationNotifier"); @@ -42,10 +45,11 @@ class AuthNotifier extends StateNotifier { this._authService, this._apiService, this._userService, + this._uploadService, this._secureStorageService, this._widgetService, ) : super( - AuthState( + const AuthState( deviceId: "", userId: "", userEmail: "", @@ -83,13 +87,14 @@ class AuthNotifier extends StateNotifier { await _widgetService.clearCredentials(); await _authService.logout(); + await _uploadService.cancelBackup(); } finally { await _cleanUp(); } } Future _cleanUp() async { - state = AuthState( + state = const AuthState( deviceId: "", userId: "", userEmail: "", @@ -124,14 +129,12 @@ class AuthNotifier extends StateNotifier { ); // Get the deviceid from the store if it exists, otherwise generate a new one - String deviceId = - Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; + String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; UserDto? user = _userService.tryGetMyUser(); try { - final serverUser = - await _userService.refreshMyUser().timeout(_timeoutDuration); + final serverUser = await _userService.refreshMyUser().timeout(_timeoutDuration); if (serverUser == null) { _log.severe("Unable to get user information from the server."); } else { diff --git a/mobile/lib/providers/background_sync.provider.dart b/mobile/lib/providers/background_sync.provider.dart index 83d103bb3b..e6e83b64df 100644 --- a/mobile/lib/providers/background_sync.provider.dart +++ b/mobile/lib/providers/background_sync.provider.dart @@ -1,8 +1,20 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; +import 'package:immich_mobile/providers/sync_status.provider.dart'; final backgroundSyncProvider = Provider((ref) { - final manager = BackgroundSyncManager(); + final syncStatusNotifier = ref.read(syncStatusProvider.notifier); + final manager = BackgroundSyncManager( + onRemoteSyncStart: syncStatusNotifier.startRemoteSync, + onRemoteSyncComplete: syncStatusNotifier.completeRemoteSync, + onRemoteSyncError: syncStatusNotifier.errorRemoteSync, + onLocalSyncStart: syncStatusNotifier.startLocalSync, + onLocalSyncComplete: syncStatusNotifier.completeLocalSync, + onLocalSyncError: syncStatusNotifier.errorLocalSync, + onHashingStart: syncStatusNotifier.startHashJob, + onHashingComplete: syncStatusNotifier.completeHashJob, + onHashingError: syncStatusNotifier.errorHashJob, + ); ref.onDispose(manager.cancel); return manager; }); diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 479a3e0bb5..69290bcfc5 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -9,8 +9,6 @@ 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/interfaces/backup_album.interface.dart'; -import 'package:immich_mobile/interfaces/file_media.interface.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'; @@ -24,6 +22,7 @@ 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'; @@ -35,8 +34,7 @@ import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; -final backupProvider = - StateNotifierProvider((ref) { +final backupProvider = StateNotifierProvider((ref) { return BackupNotifier( ref.watch(backupServiceProvider), ref.watch(serverInfoServiceProvider), @@ -75,8 +73,7 @@ class BackupNotifier extends StateNotifier { autoBackup: Store.get(StoreKey.autoBackup, false), backgroundBackup: Store.get(StoreKey.backgroundBackup, false), backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true), - backupRequireCharging: - Store.get(StoreKey.backupRequireCharging, false), + backupRequireCharging: Store.get(StoreKey.backupRequireCharging, false), backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000), serverInfo: const ServerDiskInfo( diskAvailable: "0", @@ -108,7 +105,7 @@ class BackupNotifier extends StateNotifier { final BackgroundService _backgroundService; final GalleryPermissionNotifier _galleryPermissionNotifier; final AlbumMediaRepository _albumMediaRepository; - final IFileMediaRepository _fileMediaRepository; + final FileMediaRepository _fileMediaRepository; final BackupAlbumService _backupAlbumService; final Ref ref; @@ -125,16 +122,14 @@ class BackupNotifier extends StateNotifier { removeExcludedAlbumForBackup(album); } - state = state - .copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, 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}); + state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album}); } void removeAlbumForBackup(AvailableAlbum album) { @@ -181,10 +176,7 @@ class BackupNotifier extends StateNotifier { required void Function() onBatteryInfo, }) async { assert( - enabled != null || - requireWifi != null || - requireCharging != null || - triggerDelay != null, + enabled != null || requireWifi != null || requireCharging != null || triggerDelay != null, ); final bool wasEnabled = state.backgroundBackup; final bool wasWifi = state.backupRequireWifi; @@ -258,9 +250,7 @@ class BackupNotifier extends StateNotifier { for (Album album in albums) { AvailableAlbum availableAlbum = AvailableAlbum( album: album, - assetCount: await ref - .read(albumMediaRepositoryProvider) - .getAssetCount(album.localId!), + assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(album.localId!), ); availableAlbums.add(availableAlbum); @@ -269,10 +259,8 @@ class BackupNotifier extends StateNotifier { } state = state.copyWith(availableAlbums: availableAlbums); - final List excludedBackupAlbums = - await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - final List selectedBackupAlbums = - await _backupAlbumService.getAllBySelection(BackupSelection.select); + 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) { @@ -282,8 +270,7 @@ class BackupNotifier extends StateNotifier { selectedAlbums.add( AvailableAlbum( album: albumAsset, - assetCount: - await _albumMediaRepository.getAssetCount(albumAsset.localId!), + assetCount: await _albumMediaRepository.getAssetCount(albumAsset.localId!), lastBackup: ba.lastBackup, ), ); @@ -300,9 +287,7 @@ class BackupNotifier extends StateNotifier { excludedAlbums.add( AvailableAlbum( album: albumAsset, - assetCount: await ref - .read(albumMediaRepositoryProvider) - .getAssetCount(albumAsset.localId!), + assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(albumAsset.localId!), lastBackup: ba.lastBackup, ), ); @@ -336,17 +321,13 @@ class BackupNotifier extends StateNotifier { final Set assetsFromExcludedAlbums = {}; for (final album in state.selectedBackupAlbums) { - final assetCount = await ref - .read(albumMediaRepositoryProvider) - .getAssetCount(album.album.localId!); + final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!); if (assetCount == 0) { continue; } - final assets = await ref - .read(albumMediaRepositoryProvider) - .getAssets(album.album.localId!); + final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!); // Add album's name to the asset info for (final asset in assets) { @@ -371,17 +352,13 @@ class BackupNotifier extends StateNotifier { } for (final album in state.excludedBackupAlbums) { - final assetCount = await ref - .read(albumMediaRepositoryProvider) - .getAssetCount(album.album.localId!); + final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!); if (assetCount == 0) { continue; } - final assets = await ref - .read(albumMediaRepositoryProvider) - .getAssets(album.album.localId!); + final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!); for (final asset in assets) { assetsFromExcludedAlbums.add( @@ -390,8 +367,7 @@ class BackupNotifier extends StateNotifier { } } - final Set allUniqueAssets = - assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); + final Set allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); final allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); @@ -400,11 +376,9 @@ class BackupNotifier extends StateNotifier { } // Find asset that were backup from selected albums - final Set selectedAlbumsBackupAssets = - Set.from(allUniqueAssets.map((e) => e.asset.localId)); + final Set selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.asset.localId)); - selectedAlbumsBackupAssets - .removeWhere((assetId) => !allAssetsInDatabase.contains(assetId)); + selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId)); // Remove duplicated asset from all unique assets allUniqueAssets.removeWhere( @@ -460,8 +434,7 @@ class BackupNotifier extends StateNotifier { final candidates = selected.followedBy(excluded).toList(); candidates.sortBy((e) => e.id); - final savedBackupAlbums = - await _backupAlbumService.getAll(sort: BackupAlbumSort.id); + final savedBackupAlbums = await _backupAlbumService.getAll(sort: BackupAlbumSort.id); final List toDelete = []; final List toUpsert = []; @@ -470,8 +443,7 @@ class BackupNotifier extends StateNotifier { 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; + b.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; toUpsert.add(b); return true; }, @@ -570,8 +542,7 @@ class BackupNotifier extends StateNotifier { state = state.copyWith( allUniqueAssets: state.allUniqueAssets .where( - (candidate) => - candidate.asset.localId != result.candidate.asset.localId, + (candidate) => candidate.asset.localId != result.candidate.asset.localId, ) .toSet(), ); @@ -588,21 +559,13 @@ class BackupNotifier extends StateNotifier { ); } - if (state.allUniqueAssets.length - - state.selectedAlbumsBackupAssetsIds.length == - 0) { - final latestAssetBackup = state.allUniqueAssets - .map((candidate) => candidate.asset.fileModifiedAt) - .reduce( + 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(), + 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", @@ -697,10 +660,8 @@ class BackupNotifier extends StateNotifier { } Future resumeBackup() async { - final List selectedBackupAlbums = - await _backupAlbumService.getAllBySelection(BackupSelection.select); - final List excludedBackupAlbums = - await _backupAlbumService.getAllBySelection(BackupSelection.exclude); + 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) { diff --git a/mobile/lib/providers/backup/backup_album.provider.dart b/mobile/lib/providers/backup/backup_album.provider.dart new file mode 100644 index 0000000000..1e5d424926 --- /dev/null +++ b/mobile/lib/providers/backup/backup_album.provider.dart @@ -0,0 +1,61 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/services/local_album.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; + +final backupAlbumProvider = StateNotifierProvider>( + (ref) => BackupAlbumNotifier( + ref.watch(localAlbumServiceProvider), + ), +); + +class BackupAlbumNotifier extends StateNotifier> { + BackupAlbumNotifier(this._localAlbumService) : super([]) { + getAll(); + } + + final LocalAlbumService _localAlbumService; + + Future getAll() async { + state = await _localAlbumService.getAll(sortBy: {SortLocalAlbumsBy.assetCount}); + } + + Future selectAlbum(LocalAlbum album) async { + album = album.copyWith(backupSelection: BackupSelection.selected); + await _localAlbumService.update(album); + + state = state + .map( + (currentAlbum) => currentAlbum.id == album.id + ? currentAlbum.copyWith(backupSelection: BackupSelection.selected) + : currentAlbum, + ) + .toList(); + } + + Future deselectAlbum(LocalAlbum album) async { + album = album.copyWith(backupSelection: BackupSelection.none); + await _localAlbumService.update(album); + + state = state + .map( + (currentAlbum) => + currentAlbum.id == album.id ? currentAlbum.copyWith(backupSelection: BackupSelection.none) : currentAlbum, + ) + .toList(); + } + + Future excludeAlbum(LocalAlbum album) async { + album = album.copyWith(backupSelection: BackupSelection.excluded); + await _localAlbumService.update(album); + + state = state + .map( + (currentAlbum) => currentAlbum.id == album.id + ? currentAlbum.copyWith(backupSelection: BackupSelection.excluded) + : currentAlbum, + ) + .toList(); + } +} diff --git a/mobile/lib/providers/backup/backup_verification.provider.dart b/mobile/lib/providers/backup/backup_verification.provider.dart index 5881814320..c075d81d86 100644 --- a/mobile/lib/providers/backup/backup_verification.provider.dart +++ b/mobile/lib/providers/backup/backup_verification.provider.dart @@ -23,8 +23,7 @@ class BackupVerification extends _$BackupVerification { state = true; final backupState = ref.read(backupProvider); - if (backupState.allUniqueAssets.length > - backupState.selectedAlbumsBackupAssetsIds.length) { + if (backupState.allUniqueAssets.length > backupState.selectedAlbumsBackupAssetsIds.length) { if (context.mounted) { ImmichToast.show( context: context, @@ -48,9 +47,7 @@ class BackupVerification extends _$BackupVerification { WakelockPlus.enable(); const limit = 100; - final toDelete = await ref - .read(backupVerificationServiceProvider) - .findWronglyBackedUpAssets(limit: limit); + final toDelete = await ref.read(backupVerificationServiceProvider).findWronglyBackedUpAssets(limit: limit); if (toDelete.isEmpty) { if (context.mounted) { ImmichToast.show( @@ -67,8 +64,7 @@ class BackupVerification extends _$BackupVerification { onOk: () => _performDeletion(context, toDelete), title: "Corrupt backups!", ok: "Delete", - content: - "Found ${toDelete.length} (max $limit at once) corrupt asset backups. " + 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?", ), diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart new file mode 100644 index 0000000000..d352869651 --- /dev/null +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -0,0 +1,396 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:async'; +import 'dart:convert'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/services/upload.service.dart'; + +class EnqueueStatus { + final int enqueueCount; + final int totalCount; + + const EnqueueStatus({ + required this.enqueueCount, + required this.totalCount, + }); + + EnqueueStatus copyWith({ + int? enqueueCount, + int? totalCount, + }) { + return EnqueueStatus( + enqueueCount: enqueueCount ?? this.enqueueCount, + totalCount: totalCount ?? this.totalCount, + ); + } + + @override + String toString() => 'EnqueueStatus(enqueueCount: $enqueueCount, totalCount: $totalCount)'; +} + +class DriftUploadStatus { + final String taskId; + final String filename; + final double progress; + final int fileSize; + final String networkSpeedAsString; + final bool? isFailed; + + const DriftUploadStatus({ + required this.taskId, + required this.filename, + required this.progress, + required this.fileSize, + required this.networkSpeedAsString, + this.isFailed, + }); + + DriftUploadStatus copyWith({ + String? taskId, + String? filename, + double? progress, + int? fileSize, + String? networkSpeedAsString, + bool? isFailed, + }) { + return DriftUploadStatus( + taskId: taskId ?? this.taskId, + filename: filename ?? this.filename, + progress: progress ?? this.progress, + fileSize: fileSize ?? this.fileSize, + networkSpeedAsString: networkSpeedAsString ?? this.networkSpeedAsString, + isFailed: isFailed ?? this.isFailed, + ); + } + + @override + String toString() { + return 'DriftUploadStatus(taskId: $taskId, filename: $filename, progress: $progress, fileSize: $fileSize, networkSpeedAsString: $networkSpeedAsString, isFailed: $isFailed)'; + } + + @override + bool operator ==(covariant DriftUploadStatus other) { + if (identical(this, other)) return true; + + return other.taskId == taskId && + other.filename == filename && + other.progress == progress && + other.fileSize == fileSize && + other.networkSpeedAsString == networkSpeedAsString && + other.isFailed == isFailed; + } + + @override + int get hashCode { + return taskId.hashCode ^ + filename.hashCode ^ + progress.hashCode ^ + fileSize.hashCode ^ + networkSpeedAsString.hashCode ^ + isFailed.hashCode; + } + + Map toMap() { + return { + 'taskId': taskId, + 'filename': filename, + 'progress': progress, + 'fileSize': fileSize, + 'networkSpeedAsString': networkSpeedAsString, + 'isFailed': isFailed, + }; + } + + factory DriftUploadStatus.fromMap(Map map) { + return DriftUploadStatus( + taskId: map['taskId'] as String, + filename: map['filename'] as String, + progress: map['progress'] as double, + fileSize: map['fileSize'] as int, + networkSpeedAsString: map['networkSpeedAsString'] as String, + isFailed: map['isFailed'] != null ? map['isFailed'] as bool : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory DriftUploadStatus.fromJson(String source) => + DriftUploadStatus.fromMap(json.decode(source) as Map); +} + +class DriftBackupState { + final int totalCount; + final int backupCount; + final int remainderCount; + + final int enqueueCount; + final int enqueueTotalCount; + + final bool isCanceling; + + final Map uploadItems; + + const DriftBackupState({ + required this.totalCount, + required this.backupCount, + required this.remainderCount, + required this.enqueueCount, + required this.enqueueTotalCount, + required this.isCanceling, + required this.uploadItems, + }); + + DriftBackupState copyWith({ + int? totalCount, + int? backupCount, + int? remainderCount, + int? enqueueCount, + int? enqueueTotalCount, + bool? isCanceling, + Map? uploadItems, + }) { + return DriftBackupState( + totalCount: totalCount ?? this.totalCount, + backupCount: backupCount ?? this.backupCount, + remainderCount: remainderCount ?? this.remainderCount, + enqueueCount: enqueueCount ?? this.enqueueCount, + enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount, + isCanceling: isCanceling ?? this.isCanceling, + uploadItems: uploadItems ?? this.uploadItems, + ); + } + + @override + String toString() { + return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, uploadItems: $uploadItems)'; + } + + @override + bool operator ==(covariant DriftBackupState other) { + if (identical(this, other)) return true; + final mapEquals = const DeepCollectionEquality().equals; + + return other.totalCount == totalCount && + other.backupCount == backupCount && + other.remainderCount == remainderCount && + other.enqueueCount == enqueueCount && + other.enqueueTotalCount == enqueueTotalCount && + other.isCanceling == isCanceling && + mapEquals(other.uploadItems, uploadItems); + } + + @override + int get hashCode { + return totalCount.hashCode ^ + backupCount.hashCode ^ + remainderCount.hashCode ^ + enqueueCount.hashCode ^ + enqueueTotalCount.hashCode ^ + isCanceling.hashCode ^ + uploadItems.hashCode; + } +} + +final driftBackupProvider = StateNotifierProvider((ref) { + return ExpBackupNotifier( + ref.watch(uploadServiceProvider), + ); +}); + +class ExpBackupNotifier extends StateNotifier { + ExpBackupNotifier( + this._uploadService, + ) : super( + const DriftBackupState( + totalCount: 0, + backupCount: 0, + remainderCount: 0, + enqueueCount: 0, + enqueueTotalCount: 0, + isCanceling: false, + uploadItems: {}, + ), + ) { + { + _uploadService.taskStatusStream.listen(_handleTaskStatusUpdate); + _uploadService.taskProgressStream.listen(_handleTaskProgressUpdate); + } + } + + final UploadService _uploadService; + StreamSubscription? _statusSubscription; + StreamSubscription? _progressSubscription; + + /// Remove upload item from state + void _removeUploadItem(String taskId) { + if (state.uploadItems.containsKey(taskId)) { + final updatedItems = Map.from(state.uploadItems); + updatedItems.remove(taskId); + state = state.copyWith(uploadItems: updatedItems); + } + } + + void _handleTaskStatusUpdate(TaskStatusUpdate update) { + final taskId = update.task.taskId; + + switch (update.status) { + case TaskStatus.complete: + if (update.task.group == kBackupGroup) { + state = state.copyWith( + backupCount: state.backupCount + 1, + remainderCount: state.remainderCount - 1, + ); + } + + // Remove the completed task from the upload items + if (state.uploadItems.containsKey(taskId)) { + Future.delayed(const Duration(milliseconds: 1000), () { + _removeUploadItem(taskId); + }); + } + + case TaskStatus.failed: + final currentItem = state.uploadItems[taskId]; + if (currentItem == null) { + return; + } + + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + taskId: currentItem.copyWith( + isFailed: true, + ), + }, + ); + break; + + case TaskStatus.canceled: + _removeUploadItem(update.task.taskId); + break; + + default: + break; + } + } + + void _handleTaskProgressUpdate(TaskProgressUpdate update) { + final taskId = update.task.taskId; + final filename = update.task.displayName; + final progress = update.progress; + final currentItem = state.uploadItems[taskId]; + if (currentItem != null) { + if (progress == kUploadStatusCanceled) { + _removeUploadItem(update.task.taskId); + return; + } + + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + taskId: update.hasExpectedFileSize + ? currentItem.copyWith( + progress: progress, + fileSize: update.expectedFileSize, + networkSpeedAsString: update.networkSpeedAsString, + ) + : currentItem.copyWith( + progress: progress, + ), + }, + ); + + return; + } + + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + taskId: DriftUploadStatus( + taskId: taskId, + filename: filename, + progress: progress, + fileSize: update.expectedFileSize, + networkSpeedAsString: update.networkSpeedAsString, + ), + }, + ); + } + + Future getBackupStatus(String userId) async { + final [totalCount, backupCount, remainderCount] = await Future.wait([ + _uploadService.getBackupTotalCount(), + _uploadService.getBackupFinishedCount(userId), + _uploadService.getBackupRemainderCount(userId), + ]); + + state = state.copyWith( + totalCount: totalCount, + backupCount: backupCount, + remainderCount: remainderCount, + ); + } + + Future startBackup(String userId) { + return _uploadService.startBackup(userId, _updateEnqueueCount); + } + + void _updateEnqueueCount(EnqueueStatus status) { + state = state.copyWith( + enqueueCount: status.enqueueCount, + enqueueTotalCount: status.totalCount, + ); + } + + Future cancel() async { + debugPrint("Canceling backup tasks..."); + state = state.copyWith( + enqueueCount: 0, + enqueueTotalCount: 0, + isCanceling: true, + ); + + final activeTaskCount = await _uploadService.cancelBackup(); + + if (activeTaskCount > 0) { + debugPrint( + "$activeTaskCount tasks left, continuing to cancel...", + ); + await cancel(); + } else { + debugPrint("All tasks canceled successfully."); + // Clear all upload items when cancellation is complete + state = state.copyWith( + isCanceling: false, + uploadItems: {}, + ); + } + } + + Future handleBackupResume(String userId) async { + debugPrint("handleBackupResume"); + final tasks = await _uploadService.getActiveTasks(kBackupGroup); + debugPrint("Found ${tasks.length} tasks"); + + if (tasks.isEmpty) { + // Start a new backup queue + debugPrint("Start a new backup queue"); + await startBackup(userId); + } + + debugPrint("Tasks to resume: ${tasks.length}"); + await _uploadService.resumeBackup(); + } + + @override + void dispose() { + _statusSubscription?.cancel(); + _progressSubscription?.cancel(); + super.dispose(); + } +} diff --git a/mobile/lib/providers/backup/error_backup_list.provider.dart b/mobile/lib/providers/backup/error_backup_list.provider.dart index 22ff995905..db116e4bb9 100644 --- a/mobile/lib/providers/backup/error_backup_list.provider.dart +++ b/mobile/lib/providers/backup/error_backup_list.provider.dart @@ -17,7 +17,6 @@ class ErrorBackupListNotifier extends StateNotifier> { } } -final errorBackupListProvider = - StateNotifierProvider>( +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 index 7605d73650..98d55882cc 100644 --- a/mobile/lib/providers/backup/ios_background_settings.provider.dart +++ b/mobile/lib/providers/backup/ios_background_settings.provider.dart @@ -7,7 +7,7 @@ class IOSBackgroundSettings { final DateTime? timeOfLastFetch; final DateTime? timeOfLastProcessing; - IOSBackgroundSettings({ + const IOSBackgroundSettings({ required this.appRefreshEnabled, required this.numberOfBackgroundTasksQueued, this.timeOfLastFetch, @@ -15,21 +15,17 @@ class IOSBackgroundSettings { }); } -class IOSBackgroundSettingsNotifier - extends StateNotifier { +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); + final lastFetchTime = await _service.getIOSBackupLastRun(IosBackgroundTask.fetch); + final lastProcessingTime = await _service.getIOSBackupLastRun(IosBackgroundTask.processing); int numberOfProcesses = await _service.getIOSBackupNumberOfProcesses(); - final appRefreshEnabled = - await _service.getIOSBackgroundAppRefreshEnabled(); + final appRefreshEnabled = await _service.getIOSBackgroundAppRefreshEnabled(); // If this is enabled and there are no background processes, // the user just enabled app refresh in Settings. @@ -53,7 +49,6 @@ class IOSBackgroundSettingsNotifier } } -final iOSBackgroundSettingsProvider = StateNotifierProvider< - IOSBackgroundSettingsNotifier, IOSBackgroundSettings?>( +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 index 646a03cebc..b234a4ffe2 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -31,8 +31,7 @@ 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) { +final manualUploadProvider = StateNotifierProvider((ref) { return ManualUploadNotifier( ref.watch(localNotificationService), ref.watch(backupProvider.notifier), @@ -82,8 +81,7 @@ class ManualUploadNotifier extends StateNotifier { String? _lastPrintedDetailTitle; static const notifyInterval = Duration(milliseconds: 500); - late final ThrottleProgressUpdate _throttledNotifiy = - ThrottleProgressUpdate(_updateProgress, notifyInterval); + late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval); late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate(_updateDetailProgress, notifyInterval); @@ -106,11 +104,9 @@ class ManualUploadNotifier extends StateNotifier { 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) : ""; + 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) { + if (msg != _lastPrintedDetailContent || title != _lastPrintedDetailTitle) { _lastPrintedDetailContent = msg; _lastPrintedDetailTitle = title; _localNotificationService.showOrUpdateManualUploadStatus( @@ -184,9 +180,8 @@ class ManualUploadNotifier extends StateNotifier { _throttledNotifiy(); } if (state.showDetailedNotification) { - _throttledDetailNotify.title = - "backup_background_service_current_upload_notification" - .tr(namedArgs: {'filename': currentUploadAsset.fileName}); + _throttledDetailNotify.title = "backup_background_service_current_upload_notification" + .tr(namedArgs: {'filename': currentUploadAsset.fileName}); _throttledDetailNotify.progress = 0; _throttledDetailNotify.total = 0; } @@ -200,8 +195,7 @@ class ManualUploadNotifier extends StateNotifier { if (ref.read(galleryPermissionNotifier.notifier).hasPermission) { await ref.read(fileMediaRepositoryProvider).clearFileCache(); - final allAssetsFromDevice = - allManualUploads.where((e) => e.isLocal && !e.isRemote).toList(); + final allAssetsFromDevice = allManualUploads.where((e) => e.isLocal && !e.isRemote).toList(); if (allAssetsFromDevice.length != allManualUploads.length) { _log.warning( @@ -209,14 +203,11 @@ class ManualUploadNotifier extends StateNotifier { ); } - final selectedBackupAlbums = - await _backupAlbumService.getAllBySelection(BackupSelection.select); - final excludedBackupAlbums = await _backupAlbumService - .getAllBySelection(BackupSelection.exclude); + 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( + Set candidates = await _backupService.buildUploadCandidates( selectedBackupAlbums, excludedBackupAlbums, useTimeFilter: false, @@ -260,13 +251,11 @@ class ManualUploadNotifier extends StateNotifier { } // 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); + bool showDetailedNotification = ref.read(appSettingsServiceProvider).getSetting( + AppSettingsEnum.backgroundBackupSingleProgress, + ) || + state.totalAssetsToUpload == 1; + state = state.copyWith(showDetailedNotification: showDetailedNotification); final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; final bool ok = await ref.read(backupServiceProvider).backupAsset( @@ -297,8 +286,7 @@ class ManualUploadNotifier extends StateNotifier { presentBanner: true, ); hasErrors = true; - } else if (state.successfulUploads == 0 || - (!ok && !state.cancelToken.isCancelled)) { + } else if (state.successfulUploads == 0 || (!ok && !state.cancelToken.isCancelled)) { await _localNotificationService.showOrUpdateManualUploadStatus( "backup_manual_title".tr(), "failed".tr(), @@ -334,8 +322,7 @@ class ManualUploadNotifier extends StateNotifier { 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) { + if (appState != AppLifeCycleEnum.active && appState != AppLifeCycleEnum.resumed) { ref.read(backupProvider.notifier).cancelBackup(); } } @@ -364,8 +351,7 @@ class ManualUploadNotifier extends StateNotifier { ) 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(); + final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock(); if (!hasLock) { debugPrint("[uploadAssets] could not acquire lock, exiting"); ImmichToast.show( diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index c80789d2e0..11cdcd54c5 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -1,6 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/interfaces/cast_destination_service.interface.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'; @@ -10,7 +10,7 @@ final castProvider = StateNotifierProvider( class CastNotifier extends StateNotifier { // more cast providers can be added here (ie Fcast) - final ICastDestinationService _gCastService; + final GCastService _gCastService; List<(String, CastDestinationType, dynamic)> discovered = List.empty(); @@ -51,10 +51,29 @@ class CastNotifier extends StateNotifier { state = state.copyWith(castState: castState); } - void loadMedia(Asset asset, bool reload) { + void loadMedia(RemoteAsset asset, bool reload) { _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, + ); + + _gCastService.loadMedia(remoteAsset, reload); + } + Future connect(CastDestinationType type, dynamic device) async { switch (type) { case CastDestinationType.googleCast: diff --git a/mobile/lib/providers/folder.provider.dart b/mobile/lib/providers/folder.provider.dart index 810c2cea73..7f89679734 100644 --- a/mobile/lib/providers/folder.provider.dart +++ b/mobile/lib/providers/folder.provider.dart @@ -22,9 +22,7 @@ class FolderStructureNotifier extends StateNotifier> { } } -final folderStructureProvider = - StateNotifierProvider>( - (ref) { +final folderStructureProvider = StateNotifierProvider>((ref) { return FolderStructureNotifier( ref.watch(folderServiceProvider), ); @@ -35,14 +33,12 @@ class FolderRenderListNotifier extends StateNotifier> { final RootFolder _folder; final Logger _log = Logger("FolderAssetsNotifier"); - FolderRenderListNotifier(this._folderService, this._folder) - : super(const AsyncLoading()); + FolderRenderListNotifier(this._folderService, this._folder) : super(const AsyncLoading()); Future fetchAssets(SortOrder order) async { try { final assets = await _folderService.getFolderAssets(_folder, order); - final renderList = - await RenderList.fromAssets(assets, GroupAssetsBy.none); + final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.none); state = AsyncData(renderList); } catch (e, stack) { _log.severe("Failed to fetch folder assets", e, stack); @@ -51,10 +47,8 @@ class FolderRenderListNotifier extends StateNotifier> { } } -final folderRenderListProvider = StateNotifierProvider.family< - FolderRenderListNotifier, - AsyncValue, - RootFolder>((ref, folder) { +final folderRenderListProvider = + StateNotifierProvider.family, RootFolder>((ref, folder) { return FolderRenderListNotifier( ref.watch(folderServiceProvider), folder, diff --git a/mobile/lib/providers/gallery_permission.provider.dart b/mobile/lib/providers/gallery_permission.provider.dart index 07d9cca591..3d8a6da941 100644 --- a/mobile/lib/providers/gallery_permission.provider.dart +++ b/mobile/lib/providers/gallery_permission.provider.dart @@ -5,8 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:permission_handler/permission_handler.dart'; class GalleryPermissionNotifier extends StateNotifier { - GalleryPermissionNotifier() - : super(PermissionStatus.denied) // Denied is the initial state + GalleryPermissionNotifier() : super(PermissionStatus.denied) // Denied is the initial state { // Sets the initial state getGalleryPermissionStatus(); @@ -36,8 +35,7 @@ class GalleryPermissionNotifier extends StateNotifier { // Return the joint result of those two permissions final PermissionStatus status; - if ((photos.isGranted && videos.isGranted) || - (photos.isLimited && videos.isLimited)) { + if ((photos.isGranted && videos.isGranted) || (photos.isLimited && videos.isLimited)) { status = PermissionStatus.granted; } else if (photos.isDenied || videos.isDenied) { status = PermissionStatus.denied; @@ -49,8 +47,7 @@ class GalleryPermissionNotifier extends StateNotifier { result = status; } - if (result == PermissionStatus.granted && - androidInfo.version.sdkInt >= 29) { + if (result == PermissionStatus.granted && androidInfo.version.sdkInt >= 29) { result = await Permission.accessMediaLocation.request(); } } else { @@ -80,8 +77,7 @@ class GalleryPermissionNotifier extends StateNotifier { // Return the joint result of those two permissions final PermissionStatus status; - if ((photos.isGranted && videos.isGranted) || - (photos.isLimited && videos.isLimited)) { + if ((photos.isGranted && videos.isGranted) || (photos.isLimited && videos.isLimited)) { status = PermissionStatus.granted; } else if (photos.isDenied || videos.isDenied) { status = PermissionStatus.denied; @@ -93,8 +89,7 @@ class GalleryPermissionNotifier extends StateNotifier { result = status; } - if (state == PermissionStatus.granted && - androidInfo.version.sdkInt >= 29) { + if (state == PermissionStatus.granted && androidInfo.version.sdkInt >= 29) { result = await Permission.accessMediaLocation.status; } } else { @@ -107,7 +102,6 @@ class GalleryPermissionNotifier extends StateNotifier { } } -final galleryPermissionNotifier = - StateNotifierProvider( +final galleryPermissionNotifier = StateNotifierProvider( (ref) => GalleryPermissionNotifier(), ); diff --git a/mobile/lib/providers/haptic_feedback.provider.dart b/mobile/lib/providers/haptic_feedback.provider.dart index ce8997c85c..711c6fa4e2 100644 --- a/mobile/lib/providers/haptic_feedback.provider.dart +++ b/mobile/lib/providers/haptic_feedback.provider.dart @@ -3,8 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -final hapticFeedbackProvider = - StateNotifierProvider((ref) { +final hapticFeedbackProvider = StateNotifierProvider((ref) { return HapticNotifier(ref); }); @@ -15,41 +14,31 @@ class HapticNotifier extends StateNotifier { HapticNotifier(this._ref) : super(null); selectionClick() { - if (_ref - .read(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.enableHapticFeedback)) { + if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { HapticFeedback.selectionClick(); } } lightImpact() { - if (_ref - .read(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.enableHapticFeedback)) { + if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { HapticFeedback.lightImpact(); } } mediumImpact() { - if (_ref - .read(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.enableHapticFeedback)) { + if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { HapticFeedback.mediumImpact(); } } heavyImpact() { - if (_ref - .read(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.enableHapticFeedback)) { + if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { HapticFeedback.heavyImpact(); } } vibrate() { - if (_ref - .read(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.enableHapticFeedback)) { + if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { HapticFeedback.vibrate(); } } diff --git a/mobile/lib/providers/image/cache/image_loader.dart b/mobile/lib/providers/image/cache/image_loader.dart index fd6e567b2c..f88d54e4f1 100644 --- a/mobile/lib/providers/image/cache/image_loader.dart +++ b/mobile/lib/providers/image/cache/image_loader.dart @@ -42,6 +42,6 @@ class ImageLoader { } // If we get here, the image failed to load from the cache stream - throw ImageLoadingException('Could not load image from stream'); + throw const ImageLoadingException('Could not load image from stream'); } } diff --git a/mobile/lib/providers/image/cache/thumbnail_image_cache_manager.dart b/mobile/lib/providers/image/cache/thumbnail_image_cache_manager.dart index dd7ad35277..f8d4cda3e6 100644 --- a/mobile/lib/providers/image/cache/thumbnail_image_cache_manager.dart +++ b/mobile/lib/providers/image/cache/thumbnail_image_cache_manager.dart @@ -3,8 +3,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart'; /// The cache manager for thumbnail images [ImmichRemoteThumbnailProvider] class ThumbnailImageCacheManager extends CacheManager { static const key = 'thumbnailImageCacheKey'; - static final ThumbnailImageCacheManager _instance = - ThumbnailImageCacheManager._(); + static final ThumbnailImageCacheManager _instance = ThumbnailImageCacheManager._(); factory ThumbnailImageCacheManager() { return _instance; diff --git a/mobile/lib/providers/image/exceptions/image_loading_exception.dart b/mobile/lib/providers/image/exceptions/image_loading_exception.dart index 5e5ff72359..98f633a88f 100644 --- a/mobile/lib/providers/image/exceptions/image_loading_exception.dart +++ b/mobile/lib/providers/image/exceptions/image_loading_exception.dart @@ -1,5 +1,5 @@ /// An exception for the [ImageLoader] and the Immich image providers class ImageLoadingException implements Exception { final String message; - ImageLoadingException(this.message); + const ImageLoadingException(this.message); } diff --git a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart index edcf8a9458..de69115444 100644 --- a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart +++ b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart @@ -13,8 +13,7 @@ import 'package:logging/logging.dart'; /// The local image provider for an asset /// Only viable -class ImmichLocalThumbnailProvider - extends ImageProvider { +class ImmichLocalThumbnailProvider extends ImageProvider { final Asset asset; final int height; final int width; @@ -60,13 +59,11 @@ class ImmichLocalThumbnailProvider CacheManager cache, ImageDecoderCallback decode, ) async* { - final cacheKey = - '$userId${assetData.localId}${assetData.checksum}$width$height'; + final cacheKey = '$userId${assetData.localId}${assetData.checksum}$width$height'; final fileFromCache = await cache.getFileFromCache(cacheKey); if (fileFromCache != null) { try { - final buffer = - await ui.ImmutableBuffer.fromFilePath(fileFromCache.file.path); + final buffer = await ui.ImmutableBuffer.fromFilePath(fileFromCache.file.path); final codec = await decode(buffer); yield codec; return; diff --git a/mobile/lib/providers/image/immich_remote_image_provider.dart b/mobile/lib/providers/image/immich_remote_image_provider.dart index d5189fa4fc..130b2202b4 100644 --- a/mobile/lib/providers/image/immich_remote_image_provider.dart +++ b/mobile/lib/providers/image/immich_remote_image_provider.dart @@ -15,15 +15,14 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; /// The remote image provider for full size remote images -class ImmichRemoteImageProvider - extends ImageProvider { +class ImmichRemoteImageProvider extends ImageProvider { /// The [Asset.remoteId] of the asset to fetch final String assetId; /// The image cache manager final CacheManager? cacheManager; - ImmichRemoteImageProvider({ + const ImmichRemoteImageProvider({ required this.assetId, this.cacheManager, }); diff --git a/mobile/lib/providers/image/immich_remote_thumbnail_provider.dart b/mobile/lib/providers/image/immich_remote_thumbnail_provider.dart index a1e9c737b0..cb2a6270b4 100644 --- a/mobile/lib/providers/image/immich_remote_thumbnail_provider.dart +++ b/mobile/lib/providers/image/immich_remote_thumbnail_provider.dart @@ -13,8 +13,7 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; /// The remote image provider -class ImmichRemoteThumbnailProvider - extends ImageProvider { +class ImmichRemoteThumbnailProvider extends ImageProvider { /// The [Asset.remoteId] of the asset to fetch final String assetId; @@ -24,7 +23,7 @@ class ImmichRemoteThumbnailProvider /// The image cache manager final CacheManager? cacheManager; - ImmichRemoteThumbnailProvider({ + const ImmichRemoteThumbnailProvider({ required this.assetId, this.height, this.width, diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart new file mode 100644 index 0000000000..26de1b4dba --- /dev/null +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -0,0 +1,405 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/services/action.service.dart'; +import 'package:immich_mobile/services/timeline.service.dart'; +import 'package:immich_mobile/services/upload.service.dart'; +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +final actionProvider = NotifierProvider( + ActionNotifier.new, + dependencies: [ + multiSelectProvider, + timelineServiceProvider, + ], +); + +class ActionResult { + final int count; + final bool success; + final String? error; + + const ActionResult({required this.count, required this.success, this.error}); + + @override + String toString() => 'ActionResult(count: $count, success: $success, error: $error)'; +} + +class ActionNotifier extends Notifier { + final Logger _logger = Logger('ActionNotifier'); + late ActionService _service; + late UploadService _uploadService; + + ActionNotifier() : super(); + + @override + void build() { + _uploadService = ref.watch(uploadServiceProvider); + _service = ref.watch(actionServiceProvider); + } + + List _getRemoteIdsForSource(ActionSource source) { + return _getAssets(source).whereType().toIds().toList(growable: false); + } + + List _getLocalIdsForSource(ActionSource source) { + final Set assets = _getAssets(source); + final List localIds = []; + + for (final asset in assets) { + if (asset is LocalAsset) { + localIds.add(asset.id); + } else if (asset is RemoteAsset && asset.localId != null) { + localIds.add(asset.localId!); + } + } + + return localIds; + } + + List _getOwnedRemoteIdsForSource(ActionSource source) { + final ownerId = ref.read(currentUserProvider)?.id; + return _getAssets(source).whereType().ownedAssets(ownerId).toIds().toList(growable: false); + } + + List _getOwnedRemoteAssetsForSource(ActionSource source) { + final ownerId = ref.read(currentUserProvider)?.id; + return _getIdsForSource(source).ownedAssets(ownerId).toList(); + } + + Iterable _getIdsForSource(ActionSource source) { + final Set assets = _getAssets(source); + return switch (T) { + const (RemoteAsset) => assets.whereType(), + const (LocalAsset) => assets.whereType(), + _ => const [], + } as Iterable; + } + + Set _getAssets(ActionSource source) { + return switch (source) { + ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets, + ActionSource.viewer => switch (ref.read(currentAssetNotifier)) { + BaseAsset asset => {asset}, + null => const {}, + }, + }; + } + + Future shareLink( + ActionSource source, + BuildContext context, + ) async { + final ids = _getRemoteIdsForSource(source); + try { + await _service.shareLink(ids, context); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to create shared link for assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future favorite(ActionSource source) async { + final ids = _getOwnedRemoteIdsForSource(source); + try { + await _service.favorite(ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to favorite assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future unFavorite(ActionSource source) async { + final ids = _getOwnedRemoteIdsForSource(source); + try { + await _service.unFavorite(ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to unfavorite assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future archive(ActionSource source) async { + final ids = _getOwnedRemoteIdsForSource(source); + try { + await _service.archive(ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to archive assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future unArchive(ActionSource source) async { + final ids = _getOwnedRemoteIdsForSource(source); + try { + await _service.unArchive(ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to unarchive assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future moveToLockFolder(ActionSource source) async { + final ids = _getOwnedRemoteIdsForSource(source); + final localIds = _getLocalIdsForSource(source); + try { + await _service.moveToLockFolder(ids, localIds); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to move assets to lock folder', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future removeFromLockFolder(ActionSource source) async { + final ids = _getOwnedRemoteIdsForSource(source); + try { + await _service.removeFromLockFolder(ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to remove assets from lock folder', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future trash(ActionSource source) async { + final ids = _getOwnedRemoteIdsForSource(source); + + try { + await _service.trash(ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to trash assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future restoreTrash(ActionSource source) async { + final ids = _getOwnedRemoteIdsForSource(source); + try { + await _service.restoreTrash(ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to restore trash assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future trashRemoteAndDeleteLocal(ActionSource source) async { + final ids = _getOwnedRemoteIdsForSource(source); + final localIds = _getLocalIdsForSource(source); + try { + await _service.trashRemoteAndDeleteLocal(ids, localIds); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to delete assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future deleteRemoteAndLocal(ActionSource source) async { + final ids = _getOwnedRemoteIdsForSource(source); + final localIds = _getLocalIdsForSource(source); + try { + await _service.deleteRemoteAndLocal(ids, localIds); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to delete assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future deleteLocal(ActionSource source) async { + final ids = _getLocalIdsForSource(source); + try { + final deletedCount = await _service.deleteLocal(ids); + return ActionResult(count: deletedCount, success: true); + } catch (error, stack) { + _logger.severe('Failed to delete assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future editLocation( + ActionSource source, + BuildContext context, + ) async { + final ids = _getOwnedRemoteIdsForSource(source); + try { + final isEdited = await _service.editLocation(ids, context); + if (!isEdited) { + return null; + } + + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to edit location for assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future removeFromAlbum( + ActionSource source, + String albumId, + ) async { + final ids = _getRemoteIdsForSource(source); + try { + final removedCount = await _service.removeFromAlbum(ids, albumId); + return ActionResult(count: removedCount, success: true); + } catch (error, stack) { + _logger.severe('Failed to remove assets from album', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future stack(String userId, ActionSource source) async { + final ids = _getOwnedRemoteIdsForSource(source); + try { + await _service.stack(userId, ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to stack assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future unStack(ActionSource source) async { + final assets = _getOwnedRemoteAssetsForSource(source); + try { + await _service.unStack(assets.map((e) => e.stackId).nonNulls.toList()); + return ActionResult(count: assets.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to unstack assets', error, stack); + return ActionResult( + count: assets.length, + success: false, + ); + } + } + + Future shareAssets(ActionSource source) async { + final ids = _getAssets(source).toList(growable: false); + + try { + final count = await _service.shareAssets(ids); + return ActionResult(count: count, success: true); + } catch (error, stack) { + _logger.severe('Failed to share assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future downloadAll(ActionSource source) async { + final assets = _getAssets(source).whereType().toList(growable: false); + + try { + final didEnqueue = await _service.downloadAll(assets); + final enqueueCount = didEnqueue.where((e) => e).length; + return ActionResult(count: enqueueCount, success: true); + } catch (error, stack) { + _logger.severe('Failed to download assets', error, stack); + return ActionResult( + count: assets.length, + success: false, + error: error.toString(), + ); + } + } + + Future upload(ActionSource source) async { + final assets = _getAssets(source).whereType().toList(); + try { + await _uploadService.manualBackup(assets); + return ActionResult(count: assets.length, success: true); + } catch (error, stack) { + _logger.severe('Failed manually upload assets', error, stack); + return ActionResult( + count: assets.length, + success: false, + error: error.toString(), + ); + } + } +} + +extension on Iterable { + Iterable toIds() => map((e) => e.id); + + Iterable ownedAssets(String? ownerId) { + if (ownerId == null) return const []; + return whereType().where((a) => a.ownerId == ownerId); + } +} diff --git a/mobile/lib/providers/infrastructure/album.provider.dart b/mobile/lib/providers/infrastructure/album.provider.dart index cb4aadb8a7..4baead4b75 100644 --- a/mobile/lib/providers/infrastructure/album.provider.dart +++ b/mobile/lib/providers/infrastructure/album.provider.dart @@ -1,8 +1,43 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/interfaces/local_album.interface.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/services/local_album.service.dart'; +import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; +import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; -final localAlbumRepository = Provider( +final localAlbumRepository = Provider( (ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)), ); + +final localAlbumServiceProvider = Provider( + (ref) => LocalAlbumService(ref.watch(localAlbumRepository)), +); + +final localAlbumProvider = FutureProvider>( + (ref) => LocalAlbumService(ref.watch(localAlbumRepository)).getAll(), +); + +final localAlbumThumbnailProvider = FutureProvider.family( + (ref, albumId) => LocalAlbumService(ref.watch(localAlbumRepository)).getThumbnail(albumId), +); + +final remoteAlbumRepository = Provider( + (ref) => DriftRemoteAlbumRepository(ref.watch(driftProvider)), +); + +final remoteAlbumServiceProvider = Provider( + (ref) => RemoteAlbumService( + ref.watch(remoteAlbumRepository), + ref.watch(driftAlbumApiRepositoryProvider), + ), + dependencies: [remoteAlbumRepository], +); + +final remoteAlbumProvider = NotifierProvider( + RemoteAlbumNotifier.new, + dependencies: [remoteAlbumServiceProvider], +); diff --git a/mobile/lib/providers/infrastructure/asset.provider.dart b/mobile/lib/providers/infrastructure/asset.provider.dart index d714571473..102e6aa60c 100644 --- a/mobile/lib/providers/infrastructure/asset.provider.dart +++ b/mobile/lib/providers/infrastructure/asset.provider.dart @@ -1,8 +1,27 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart'; +import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -final localAssetRepository = Provider( +final localAssetRepository = Provider( (ref) => DriftLocalAssetRepository(ref.watch(driftProvider)), ); + +final remoteAssetRepositoryProvider = Provider( + (ref) => RemoteAssetRepository(ref.watch(driftProvider)), +); + +final assetServiceProvider = Provider( + (ref) => AssetService( + remoteAssetRepository: ref.watch(remoteAssetRepositoryProvider), + localAssetRepository: ref.watch(localAssetRepository), + ), +); + +final placesProvider = FutureProvider>( + (ref) => AssetService( + remoteAssetRepository: ref.watch(remoteAssetRepositoryProvider), + localAssetRepository: ref.watch(localAssetRepository), + ).getPlaces(), +); diff --git a/mobile/lib/providers/infrastructure/asset_face.provider.dart b/mobile/lib/providers/infrastructure/asset_face.provider.dart new file mode 100644 index 0000000000..386609ba94 --- /dev/null +++ b/mobile/lib/providers/infrastructure/asset_face.provider.dart @@ -0,0 +1,7 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/asset_face.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final driftAssetFaceProvider = Provider( + (ref) => DriftAssetFaceRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/providers/infrastructure/asset_viewer/current_asset.provider.dart b/mobile/lib/providers/infrastructure/asset_viewer/current_asset.provider.dart new file mode 100644 index 0000000000..66de676c08 --- /dev/null +++ b/mobile/lib/providers/infrastructure/asset_viewer/current_asset.provider.dart @@ -0,0 +1,44 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; + +final currentAssetNotifier = AutoDisposeNotifierProvider( + CurrentAssetNotifier.new, +); + +class CurrentAssetNotifier extends AutoDisposeNotifier { + KeepAliveLink? _keepAliveLink; + StreamSubscription? _assetSubscription; + + @override + BaseAsset? build() => null; + + void setAsset(BaseAsset asset) { + _keepAliveLink?.close(); + _assetSubscription?.cancel(); + state = asset; + _assetSubscription = ref.watch(assetServiceProvider).watchAsset(asset).listen((updatedAsset) { + if (updatedAsset != null) { + state = updatedAsset; + } + }); + _keepAliveLink = ref.keepAlive(); + } + + void dispose() { + _keepAliveLink?.close(); + _assetSubscription?.cancel(); + } +} + +final currentAssetExifProvider = FutureProvider.autoDispose( + (ref) { + final currentAsset = ref.watch(currentAssetNotifier); + if (currentAsset == null) { + return null; + } + return ref.watch(assetServiceProvider).getExif(currentAsset); + }, +); diff --git a/mobile/lib/providers/infrastructure/current_album.provider.dart b/mobile/lib/providers/infrastructure/current_album.provider.dart new file mode 100644 index 0000000000..5d6e0414b0 --- /dev/null +++ b/mobile/lib/providers/infrastructure/current_album.provider.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; + +final currentRemoteAlbumProvider = AutoDisposeNotifierProvider( + CurrentAlbumNotifier.new, +); + +class CurrentAlbumNotifier extends AutoDisposeNotifier { + KeepAliveLink? _keepAliveLink; + StreamSubscription? _assetSubscription; + + @override + RemoteAlbum? build() => null; + + void setAlbum(RemoteAlbum album) { + _keepAliveLink?.close(); + _assetSubscription?.cancel(); + state = album; + + _assetSubscription = ref.watch(remoteAlbumServiceProvider).watchAlbum(album.id).listen((updatedAlbum) { + if (updatedAlbum != null) { + state = updatedAlbum; + } + }); + _keepAliveLink = ref.keepAlive(); + } + + void dispose() { + _keepAliveLink?.close(); + _assetSubscription?.cancel(); + } +} diff --git a/mobile/lib/providers/infrastructure/db.provider.dart b/mobile/lib/providers/infrastructure/db.provider.dart index 4eefbc556c..cdf934e508 100644 --- a/mobile/lib/providers/infrastructure/db.provider.dart +++ b/mobile/lib/providers/infrastructure/db.provider.dart @@ -13,5 +13,6 @@ Isar isar(Ref ref) => throw UnimplementedError('isar'); final driftProvider = Provider((ref) { final drift = Drift(); ref.onDispose(() => unawaited(drift.close())); + ref.keepAlive(); return drift; }); diff --git a/mobile/lib/providers/infrastructure/device_asset.provider.dart b/mobile/lib/providers/infrastructure/device_asset.provider.dart index 5fa532b9ec..7854af016a 100644 --- a/mobile/lib/providers/infrastructure/device_asset.provider.dart +++ b/mobile/lib/providers/infrastructure/device_asset.provider.dart @@ -1,8 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart'; import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -final deviceAssetRepositoryProvider = Provider( +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 index ecb67dd2fe..c126f6cac0 100644 --- a/mobile/lib/providers/infrastructure/exif.provider.dart +++ b/mobile/lib/providers/infrastructure/exif.provider.dart @@ -1,5 +1,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/interfaces/exif.interface.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'; @@ -7,5 +6,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'exif.provider.g.dart'; @Riverpod(keepAlive: true) -IExifInfoRepository exifRepository(Ref ref) => - IsarExifRepository(ref.watch(isarProvider)); +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 index 053abf18cc..0261558707 100644 --- a/mobile/lib/providers/infrastructure/exif.provider.g.dart +++ b/mobile/lib/providers/infrastructure/exif.provider.g.dart @@ -6,11 +6,11 @@ part of 'exif.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$exifRepositoryHash() => r'f0abe778ed61fbb257001fdf2ac6e17814011fee'; +String _$exifRepositoryHash() => r'bf4a3f6a50d954a23d317659b4f3e2f381066463'; /// See also [exifRepository]. @ProviderFor(exifRepository) -final exifRepositoryProvider = Provider.internal( +final exifRepositoryProvider = Provider.internal( exifRepository, name: r'exifRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -22,6 +22,6 @@ final exifRepositoryProvider = Provider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef ExifRepositoryRef = ProviderRef; +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 new file mode 100644 index 0000000000..e5809a12b4 --- /dev/null +++ b/mobile/lib/providers/infrastructure/memory.provider.dart @@ -0,0 +1,26 @@ +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/user.provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'db.provider.dart'; + +final driftMemoryRepositoryProvider = Provider( + (ref) => DriftMemoryRepository(ref.watch(driftProvider)), +); + +final driftMemoryServiceProvider = Provider( + (ref) => DriftMemoryService(ref.watch(driftMemoryRepositoryProvider)), +); + +final driftMemoryFutureProvider = FutureProvider.autoDispose>((ref) async { + final user = ref.watch(currentUserProvider); + if (user == null) { + return []; + } + + final service = ref.watch(driftMemoryServiceProvider); + + return service.getMemoryLane(user.id); +}); diff --git a/mobile/lib/providers/infrastructure/partner.provider.dart b/mobile/lib/providers/infrastructure/partner.provider.dart new file mode 100644 index 0000000000..f4ba4cc73a --- /dev/null +++ b/mobile/lib/providers/infrastructure/partner.provider.dart @@ -0,0 +1,87 @@ +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; + + @override + List build() { + _driftPartnerService = ref.read(driftPartnerServiceProvider); + return []; + } + + Future _loadPartners() async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + state = await _driftPartnerService.getSharedWith(currentUser.id); + } + + Future> getPartners(String userId) async { + final partners = await _driftPartnerService.getSharedWith(userId); + state = partners; + return partners; + } + + Future toggleShowInTimeline(String partnerId, String userId) async { + await _driftPartnerService.toggleShowInTimeline(partnerId, userId); + await _loadPartners(); + } + + Future addPartner(PartnerUserDto partner) async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + await _driftPartnerService.addPartner(partner.id, currentUser.id); + await _loadPartners(); + ref.invalidate(driftAvailablePartnerProvider); + ref.invalidate(driftSharedByPartnerProvider); + } + + Future removePartner(PartnerUserDto partner) async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + await _driftPartnerService.removePartner(partner.id, currentUser.id); + await _loadPartners(); + ref.invalidate(driftAvailablePartnerProvider); + ref.invalidate(driftSharedByPartnerProvider); + } +} + +final driftAvailablePartnerProvider = FutureProvider.autoDispose>((ref) { + final currentUser = ref.watch(currentUserProvider); + if (currentUser == null) { + return []; + } + + return ref.watch(driftPartnerServiceProvider).getAvailablePartners(currentUser.id); +}); + +final driftSharedByPartnerProvider = FutureProvider.autoDispose>((ref) { + final currentUser = ref.watch(currentUserProvider); + if (currentUser == null) { + return []; + } + + return ref.watch(driftPartnerServiceProvider).getSharedBy(currentUser.id); +}); + +final driftSharedWithPartnerProvider = FutureProvider.autoDispose>((ref) { + final currentUser = ref.watch(currentUserProvider); + if (currentUser == null) { + return []; + } + + return ref.watch(driftPartnerServiceProvider).getSharedWith(currentUser.id); +}); diff --git a/mobile/lib/providers/infrastructure/person.provider.dart b/mobile/lib/providers/infrastructure/person.provider.dart new file mode 100644 index 0000000000..a733104b33 --- /dev/null +++ b/mobile/lib/providers/infrastructure/person.provider.dart @@ -0,0 +1,7 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/person.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final driftPersonProvider = Provider( + (ref) => DriftPersonRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart new file mode 100644 index 0000000000..c6d9337b53 --- /dev/null +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -0,0 +1,221 @@ +import 'package:collection/collection.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/services/remote_album.service.dart'; +import 'package:immich_mobile/models/albums/album_search.model.dart'; +import 'package:immich_mobile/utils/remote_album.utils.dart'; +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'album.provider.dart'; + +class RemoteAlbumState { + final List albums; + final List filteredAlbums; + + const RemoteAlbumState({ + required this.albums, + List? filteredAlbums, + }) : filteredAlbums = filteredAlbums ?? albums; + + RemoteAlbumState copyWith({ + List? albums, + List? filteredAlbums, + }) { + return RemoteAlbumState( + albums: albums ?? this.albums, + filteredAlbums: filteredAlbums ?? this.filteredAlbums, + ); + } + + @override + String toString() => 'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length})'; + + @override + bool operator ==(covariant RemoteAlbumState other) { + if (identical(this, other)) return true; + final listEquals = const DeepCollectionEquality().equals; + + return listEquals(other.albums, albums) && listEquals(other.filteredAlbums, filteredAlbums); + } + + @override + int get hashCode => albums.hashCode ^ filteredAlbums.hashCode; +} + +class RemoteAlbumNotifier extends Notifier { + late RemoteAlbumService _remoteAlbumService; + final _logger = Logger('RemoteAlbumNotifier'); + @override + RemoteAlbumState build() { + _remoteAlbumService = ref.read(remoteAlbumServiceProvider); + return const RemoteAlbumState(albums: [], filteredAlbums: []); + } + + Future> _getAll() async { + try { + final albums = await _remoteAlbumService.getAll(); + state = state.copyWith( + albums: albums, + filteredAlbums: albums, + ); + return albums; + } catch (error, stack) { + _logger.severe('Failed to fetch albums', error, stack); + rethrow; + } + } + + Future refresh() async { + await _getAll(); + } + + void searchAlbums( + String query, + String? userId, [ + QuickFilterMode filterMode = QuickFilterMode.all, + ]) { + final filtered = _remoteAlbumService.searchAlbums( + state.albums, + query, + userId, + filterMode, + ); + + state = state.copyWith( + filteredAlbums: filtered, + ); + } + + void clearSearch() { + state = state.copyWith( + filteredAlbums: state.albums, + ); + } + + void sortFilteredAlbums( + RemoteAlbumSortMode sortMode, { + bool isReverse = false, + }) { + final sortedAlbums = _remoteAlbumService.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse); + state = state.copyWith(filteredAlbums: sortedAlbums); + } + + Future createAlbum({ + required String title, + String? description, + List assetIds = const [], + }) async { + try { + final album = await _remoteAlbumService.createAlbum( + title: title, + description: description, + assetIds: assetIds, + ); + + state = state.copyWith( + albums: [...state.albums, album], + filteredAlbums: [...state.filteredAlbums, album], + ); + + return album; + } catch (error, stack) { + _logger.severe('Failed to create album', error, stack); + rethrow; + } + } + + Future updateAlbum( + String albumId, { + String? name, + String? description, + String? thumbnailAssetId, + bool? isActivityEnabled, + AlbumAssetOrder? order, + }) async { + try { + final updatedAlbum = await _remoteAlbumService.updateAlbum( + albumId, + name: name, + description: description, + thumbnailAssetId: thumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: order, + ); + + final updatedAlbums = state.albums.map((album) { + return album.id == albumId ? updatedAlbum : album; + }).toList(); + + final updatedFilteredAlbums = state.filteredAlbums.map((album) { + return album.id == albumId ? updatedAlbum : album; + }).toList(); + + state = state.copyWith( + albums: updatedAlbums, + filteredAlbums: updatedFilteredAlbums, + ); + + return updatedAlbum; + } catch (error, stack) { + _logger.severe('Failed to update album', error, stack); + rethrow; + } + } + + Future toggleAlbumOrder(String albumId) async { + final currentAlbum = state.albums.firstWhere((album) => album.id == albumId); + + final newOrder = currentAlbum.order == AlbumAssetOrder.asc ? AlbumAssetOrder.desc : AlbumAssetOrder.asc; + + return updateAlbum(albumId, order: newOrder); + } + + Future deleteAlbum(String albumId) async { + await _remoteAlbumService.deleteAlbum(albumId); + + final updatedAlbums = state.albums.where((album) => album.id != albumId).toList(); + final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList(); + + state = state.copyWith( + albums: updatedAlbums, + filteredAlbums: updatedFilteredAlbums, + ); + } + + Future> getAssets(String albumId) { + return _remoteAlbumService.getAssets(albumId); + } + + Future addAssets(String albumId, List assetIds) { + return _remoteAlbumService.addAssets( + albumId: albumId, + assetIds: assetIds, + ); + } + + Future addUsers(String albumId, List userIds) { + return _remoteAlbumService.addUsers( + albumId: albumId, + userIds: userIds, + ); + } +} + +final remoteAlbumDateRangeProvider = FutureProvider.family<(DateTime, DateTime), String>( + (ref, albumId) async { + final service = ref.watch(remoteAlbumServiceProvider); + return service.getDateRange(albumId); + }, +); + +final remoteAlbumSharedUsersProvider = FutureProvider.autoDispose.family, String>( + (ref, albumId) async { + final link = ref.keepAlive(); + ref.onDispose(() => link.close()); + final service = ref.watch(remoteAlbumServiceProvider); + return service.getSharedUsers(albumId); + }, +); diff --git a/mobile/lib/providers/infrastructure/search.provider.dart b/mobile/lib/providers/infrastructure/search.provider.dart new file mode 100644 index 0000000000..cdcd3ee43b --- /dev/null +++ b/mobile/lib/providers/infrastructure/search.provider.dart @@ -0,0 +1,12 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/search.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; + +final searchApiRepositoryProvider = Provider( + (ref) => SearchApiRepository(ref.watch(apiServiceProvider).searchApi), +); + +final searchServiceProvider = Provider( + (ref) => SearchService(ref.watch(searchApiRepositoryProvider)), +); diff --git a/mobile/lib/providers/infrastructure/setting.provider.dart b/mobile/lib/providers/infrastructure/setting.provider.dart index ad0af8282e..7d8be72cd0 100644 --- a/mobile/lib/providers/infrastructure/setting.provider.dart +++ b/mobile/lib/providers/infrastructure/setting.provider.dart @@ -5,8 +5,7 @@ import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; class SettingsNotifier extends Notifier { @override - SettingsService build() => - SettingsService(storeService: ref.read(storeServiceProvider)); + SettingsService build() => SettingsService(storeService: ref.read(storeServiceProvider)); T get(Setting setting) => state.get(setting); @@ -18,5 +17,4 @@ class SettingsNotifier extends Notifier { Stream watch(Setting setting) => state.watch(setting); } -final settingsProvider = - NotifierProvider(SettingsNotifier.new); +final settingsProvider = NotifierProvider(SettingsNotifier.new); diff --git a/mobile/lib/providers/infrastructure/stack.provider.dart b/mobile/lib/providers/infrastructure/stack.provider.dart new file mode 100644 index 0000000000..71abd1e87a --- /dev/null +++ b/mobile/lib/providers/infrastructure/stack.provider.dart @@ -0,0 +1,7 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/stack.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final driftStackProvider = Provider( + (ref) => DriftStackRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/providers/infrastructure/storage.provider.dart b/mobile/lib/providers/infrastructure/storage.provider.dart index d8ac79f1c1..5bbbe51497 100644 --- a/mobile/lib/providers/infrastructure/storage.provider.dart +++ b/mobile/lib/providers/infrastructure/storage.provider.dart @@ -1,7 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/interfaces/storage.interface.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; -final storageRepositoryProvider = Provider( - (ref) => StorageRepository(), +final storageRepositoryProvider = Provider( + (ref) => const StorageRepository(), ); diff --git a/mobile/lib/providers/infrastructure/store.provider.dart b/mobile/lib/providers/infrastructure/store.provider.dart index c7f0c04a4f..0bf42f3e8b 100644 --- a/mobile/lib/providers/infrastructure/store.provider.dart +++ b/mobile/lib/providers/infrastructure/store.provider.dart @@ -1,5 +1,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/interfaces/store.interface.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'; @@ -8,8 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'store.provider.g.dart'; @Riverpod(keepAlive: true) -IStoreRepository storeRepository(Ref ref) => - IsarStoreRepository(ref.watch(isarProvider)); +IsarStoreRepository storeRepository(Ref ref) => IsarStoreRepository(ref.watch(isarProvider)); @Riverpod(keepAlive: true) StoreService storeService(Ref _) => StoreService.I; diff --git a/mobile/lib/providers/infrastructure/store.provider.g.dart b/mobile/lib/providers/infrastructure/store.provider.g.dart index ffdcd291b6..22b783013a 100644 --- a/mobile/lib/providers/infrastructure/store.provider.g.dart +++ b/mobile/lib/providers/infrastructure/store.provider.g.dart @@ -6,11 +6,11 @@ part of 'store.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$storeRepositoryHash() => r'99d24875d30c5e86b1c6caa352a0026167114e62'; +String _$storeRepositoryHash() => r'659cb134466e4b0d5f04e2fc93e426350d99545f'; /// See also [storeRepository]. @ProviderFor(storeRepository) -final storeRepositoryProvider = Provider.internal( +final storeRepositoryProvider = Provider.internal( storeRepository, name: r'storeRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -22,7 +22,7 @@ final storeRepositoryProvider = Provider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef StoreRepositoryRef = ProviderRef; +typedef StoreRepositoryRef = ProviderRef; String _$storeServiceHash() => r'250e10497c42df360e9e1f9a618d0b19c1b5b0a0'; /// See also [storeService]. diff --git a/mobile/lib/providers/infrastructure/sync.provider.dart b/mobile/lib/providers/infrastructure/sync.provider.dart index 706deacb04..2406c37fa8 100644 --- a/mobile/lib/providers/infrastructure/sync.provider.dart +++ b/mobile/lib/providers/infrastructure/sync.provider.dart @@ -11,7 +11,6 @@ import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; final syncStreamServiceProvider = Provider( (ref) => SyncStreamService( @@ -33,7 +32,6 @@ final localSyncServiceProvider = Provider( (ref) => LocalSyncService( localAlbumRepository: ref.watch(localAlbumRepository), nativeSyncApi: ref.watch(nativeSyncApiProvider), - storeService: ref.watch(storeServiceProvider), ), ); diff --git a/mobile/lib/providers/infrastructure/timeline.provider.dart b/mobile/lib/providers/infrastructure/timeline.provider.dart index 7004dd0262..1f8c344f31 100644 --- a/mobile/lib/providers/infrastructure/timeline.provider.dart +++ b/mobile/lib/providers/infrastructure/timeline.provider.dart @@ -1,23 +1,29 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/interfaces/timeline.interface.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; -final timelineRepositoryProvider = Provider( +final timelineRepositoryProvider = Provider( (ref) => DriftTimelineRepository(ref.watch(driftProvider)), ); final timelineArgsProvider = Provider.autoDispose( - (ref) => - throw UnimplementedError('Will be overridden through a ProviderScope.'), + (ref) => throw UnimplementedError('Will be overridden through a ProviderScope.'), ); -final timelineServiceProvider = Provider.autoDispose( - (ref) => - throw UnimplementedError('Will be overridden through a ProviderScope.'), +final timelineServiceProvider = Provider( + (ref) { + final timelineUsers = ref.watch(timelineUsersProvider).valueOrNull ?? []; + final timelineService = ref.watch(timelineFactoryProvider).main(timelineUsers); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + // Empty dependencies to inform the framework that this provider + // might be used in a ProviderScope + dependencies: [], ); final timelineFactoryProvider = Provider( @@ -26,3 +32,14 @@ final timelineFactoryProvider = Provider( settingsService: ref.watch(settingsProvider), ), ); + +final timelineUsersProvider = StreamProvider>( + (ref) { + final currentUserId = ref.watch(currentUserProvider.select((u) => u?.id)); + if (currentUserId == null) { + return Stream.value([]); + } + + return ref.watch(timelineRepositoryProvider).watchTimelineUserIds(currentUserId); + }, +); diff --git a/mobile/lib/providers/infrastructure/user.provider.dart b/mobile/lib/providers/infrastructure/user.provider.dart index ca65f8be14..cd62be2bec 100644 --- a/mobile/lib/providers/infrastructure/user.provider.dart +++ b/mobile/lib/providers/infrastructure/user.provider.dart @@ -1,21 +1,24 @@ 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/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'; @Riverpod(keepAlive: true) -IsarUserRepository userRepository(Ref ref) => - IsarUserRepository(ref.watch(isarProvider)); +IsarUserRepository userRepository(Ref ref) => IsarUserRepository(ref.watch(isarProvider)); @Riverpod(keepAlive: true) -UserApiRepository userApiRepository(Ref ref) => - UserApiRepository(ref.watch(apiServiceProvider).usersApi); +UserApiRepository userApiRepository(Ref ref) => UserApiRepository(ref.watch(apiServiceProvider).usersApi); @Riverpod(keepAlive: true) UserService userService(Ref ref) => UserService( @@ -23,3 +26,19 @@ UserService userService(Ref ref) => UserService( userApiRepository: ref.watch(userApiRepositoryProvider), storeService: ref.watch(storeServiceProvider), ); + +/// Drifts +final driftPartnerRepositoryProvider = Provider( + (ref) => DriftPartnerRepository(ref.watch(driftProvider)), +); + +final driftPartnerServiceProvider = Provider( + (ref) => DriftPartnerService( + ref.watch(driftPartnerRepositoryProvider), + ref.watch(partnerApiRepositoryProvider), + ), +); + +final partnerUsersProvider = NotifierProvider>( + PartnerNotifier.new, +); diff --git a/mobile/lib/providers/infrastructure/user_metadata.provider.dart b/mobile/lib/providers/infrastructure/user_metadata.provider.dart new file mode 100644 index 0000000000..2e2ae7555b --- /dev/null +++ b/mobile/lib/providers/infrastructure/user_metadata.provider.dart @@ -0,0 +1,7 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final userMetadataRepository = Provider( + (ref) => DriftUserMetadataRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/providers/local_auth.provider.dart b/mobile/lib/providers/local_auth.provider.dart index 6f7ca5eb71..56a5b2191b 100644 --- a/mobile/lib/providers/local_auth.provider.dart +++ b/mobile/lib/providers/local_auth.provider.dart @@ -9,8 +9,7 @@ import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; import 'package:logging/logging.dart'; -final localAuthProvider = - StateNotifierProvider((ref) { +final localAuthProvider = StateNotifierProvider((ref) { return LocalAuthNotifier( ref.watch(localAuthServiceProvider), ref.watch(secureStorageServiceProvider), @@ -39,8 +38,7 @@ class LocalAuthNotifier extends StateNotifier { } Future registerBiometric(BuildContext context, String pinCode) async { - final isAuthenticated = - await authenticate(context, 'Authenticate to enable biometrics'); + final isAuthenticated = await authenticate(context, 'Authenticate to enable biometrics'); if (!isAuthenticated) { return false; diff --git a/mobile/lib/providers/map/map_marker.provider.dart b/mobile/lib/providers/map/map_marker.provider.dart index 23342b77b3..e107dd3602 100644 --- a/mobile/lib/providers/map/map_marker.provider.dart +++ b/mobile/lib/providers/map/map_marker.provider.dart @@ -12,26 +12,17 @@ Future> mapMarkers(Ref ref) async { final mapState = ref.read(mapStateNotifierProvider); DateTime? fileCreatedAfter; bool? isFavorite; - bool? isIncludeArchived; - bool? isWithPartners; + bool isIncludeArchived = mapState.includeArchived; + bool isWithPartners = mapState.withPartners; if (mapState.relativeTime != 0) { - fileCreatedAfter = - DateTime.now().subtract(Duration(days: mapState.relativeTime)); + fileCreatedAfter = DateTime.now().subtract(Duration(days: mapState.relativeTime)); } if (mapState.showFavoriteOnly) { isFavorite = true; } - if (!mapState.includeArchived) { - isIncludeArchived = false; - } - - if (mapState.withPartners) { - isWithPartners = true; - } - final markers = await service.getMapMarkers( isFavorite: isFavorite, withArchived: isIncludeArchived, diff --git a/mobile/lib/providers/map/map_marker.provider.g.dart b/mobile/lib/providers/map/map_marker.provider.g.dart index 76cc44a103..a4c1db7dc0 100644 --- a/mobile/lib/providers/map/map_marker.provider.g.dart +++ b/mobile/lib/providers/map/map_marker.provider.g.dart @@ -6,7 +6,7 @@ part of 'map_marker.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$mapMarkersHash() => r'f33ac4baa3251b3f06423aece89673315966f885'; +String _$mapMarkersHash() => r'a0c129fcddbf1b9bce4aafcd2e47a858ab6ef1c9'; /// See also [mapMarkers]. @ProviderFor(mapMarkers) diff --git a/mobile/lib/providers/map/map_state.provider.dart b/mobile/lib/providers/map/map_state.provider.dart index 189a23cd0a..4337654be0 100644 --- a/mobile/lib/providers/map/map_state.provider.dart +++ b/mobile/lib/providers/map/map_state.provider.dart @@ -13,22 +13,15 @@ class MapStateNotifier extends _$MapStateNotifier { MapState build() { final appSettingsProvider = ref.read(appSettingsServiceProvider); - final lightStyleUrl = - ref.read(serverInfoProvider).serverConfig.mapLightStyleUrl; - final darkStyleUrl = - ref.read(serverInfoProvider).serverConfig.mapDarkStyleUrl; + final lightStyleUrl = ref.read(serverInfoProvider).serverConfig.mapLightStyleUrl; + final darkStyleUrl = ref.read(serverInfoProvider).serverConfig.mapDarkStyleUrl; return MapState( - themeMode: ThemeMode.values[ - appSettingsProvider.getSetting(AppSettingsEnum.mapThemeMode)], - showFavoriteOnly: appSettingsProvider - .getSetting(AppSettingsEnum.mapShowFavoriteOnly), - includeArchived: appSettingsProvider - .getSetting(AppSettingsEnum.mapIncludeArchived), - withPartners: - appSettingsProvider.getSetting(AppSettingsEnum.mapwithPartners), - relativeTime: - appSettingsProvider.getSetting(AppSettingsEnum.mapRelativeDate), + themeMode: ThemeMode.values[appSettingsProvider.getSetting(AppSettingsEnum.mapThemeMode)], + showFavoriteOnly: appSettingsProvider.getSetting(AppSettingsEnum.mapShowFavoriteOnly), + includeArchived: appSettingsProvider.getSetting(AppSettingsEnum.mapIncludeArchived), + withPartners: appSettingsProvider.getSetting(AppSettingsEnum.mapwithPartners), + relativeTime: appSettingsProvider.getSetting(AppSettingsEnum.mapRelativeDate), lightStyleFetched: AsyncData(lightStyleUrl), darkStyleFetched: AsyncData(darkStyleUrl), ); diff --git a/mobile/lib/providers/memory.provider.dart b/mobile/lib/providers/memory.provider.dart index aed546002d..7fef3060cc 100644 --- a/mobile/lib/providers/memory.provider.dart +++ b/mobile/lib/providers/memory.provider.dart @@ -2,8 +2,7 @@ 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 memoryFutureProvider = FutureProvider.autoDispose?>((ref) async { final service = ref.watch(memoryServiceProvider); return await service.getMemoryLane(); diff --git a/mobile/lib/providers/notification_permission.provider.dart b/mobile/lib/providers/notification_permission.provider.dart index 608f35d63f..e293452390 100644 --- a/mobile/lib/providers/notification_permission.provider.dart +++ b/mobile/lib/providers/notification_permission.provider.dart @@ -6,9 +6,7 @@ import 'package:permission_handler/permission_handler.dart'; class NotificationPermissionNotifier extends StateNotifier { NotificationPermissionNotifier() : super( - Platform.isAndroid - ? PermissionStatus.granted - : PermissionStatus.restricted, + Platform.isAndroid ? PermissionStatus.granted : PermissionStatus.restricted, ) { // Sets the initial state getNotificationPermission().then((p) => state = p); @@ -40,7 +38,6 @@ class NotificationPermissionNotifier extends StateNotifier { } } -final notificationPermissionProvider = - StateNotifierProvider( +final notificationPermissionProvider = StateNotifierProvider( (ref) => NotificationPermissionNotifier(), ); diff --git a/mobile/lib/providers/oauth.provider.dart b/mobile/lib/providers/oauth.provider.dart index d8d66122f7..14b3353943 100644 --- a/mobile/lib/providers/oauth.provider.dart +++ b/mobile/lib/providers/oauth.provider.dart @@ -2,5 +2,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/services/oauth.service.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -final oAuthServiceProvider = - Provider((ref) => OAuthService(ref.watch(apiServiceProvider))); +final oAuthServiceProvider = Provider((ref) => OAuthService(ref.watch(apiServiceProvider))); diff --git a/mobile/lib/providers/partner.provider.dart b/mobile/lib/providers/partner.provider.dart index f210c7fe3f..37e07958d3 100644 --- a/mobile/lib/providers/partner.provider.dart +++ b/mobile/lib/providers/partner.provider.dart @@ -38,8 +38,7 @@ class PartnerSharedWithNotifier extends StateNotifier> { } } -final partnerSharedWithProvider = - StateNotifierProvider>((ref) { +final partnerSharedWithProvider = StateNotifierProvider>((ref) { return PartnerSharedWithNotifier( ref.watch(partnerServiceProvider), ); @@ -73,13 +72,11 @@ class PartnerSharedByNotifier extends StateNotifier> { } } -final partnerSharedByProvider = - StateNotifierProvider>((ref) { +final partnerSharedByProvider = StateNotifierProvider>((ref) { return PartnerSharedByNotifier(ref.watch(partnerServiceProvider)); }); -final partnerAvailableProvider = - FutureProvider.autoDispose>((ref) async { +final partnerAvailableProvider = FutureProvider.autoDispose>((ref) async { final otherUsers = await ref.watch(otherUsersProvider.future); final currentPartners = ref.watch(partnerSharedByProvider); final available = Set.of(otherUsers); diff --git a/mobile/lib/providers/routes.provider.dart b/mobile/lib/providers/routes.provider.dart index a5b903e312..74d86f4767 100644 --- a/mobile/lib/providers/routes.provider.dart +++ b/mobile/lib/providers/routes.provider.dart @@ -1,3 +1,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; final inLockedViewProvider = StateProvider((ref) => false); +final currentRouteNameProvider = StateProvider((ref) => null); diff --git a/mobile/lib/providers/search/paginated_search.provider.dart b/mobile/lib/providers/search/paginated_search.provider.dart index bac5c5e77e..dbeacb45c6 100644 --- a/mobile/lib/providers/search/paginated_search.provider.dart +++ b/mobile/lib/providers/search/paginated_search.provider.dart @@ -8,16 +8,14 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'paginated_search.provider.g.dart'; -final paginatedSearchProvider = - StateNotifierProvider( +final paginatedSearchProvider = StateNotifierProvider( (ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)), ); class PaginatedSearchNotifier extends StateNotifier { final SearchService _searchService; - PaginatedSearchNotifier(this._searchService) - : super(SearchResult(assets: [], nextPage: 1)); + PaginatedSearchNotifier(this._searchService) : super(const SearchResult(assets: [], nextPage: 1)); Future search(SearchFilter filter) async { if (state.nextPage == null) { @@ -39,7 +37,7 @@ class PaginatedSearchNotifier extends StateNotifier { } clear() { - state = SearchResult(assets: [], nextPage: 1); + state = const SearchResult(assets: [], nextPage: 1); } } diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index d03d533aaf..f0faabd35a 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -9,7 +9,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'people.provider.g.dart'; @riverpod -Future> getAllPeople( +Future> getAllPeople( Ref ref, ) async { final PersonService personService = ref.read(personServiceProvider); @@ -25,8 +25,7 @@ Future personAssets(Ref ref, String personId) async { final assets = await personService.getPersonAssets(personId); final settings = ref.read(appSettingsServiceProvider); - final groupBy = - GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; + final groupBy = GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; return await RenderList.fromAssets(assets, groupBy); } diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart index 391edd362c..4625891abb 100644 --- a/mobile/lib/providers/search/people.provider.g.dart +++ b/mobile/lib/providers/search/people.provider.g.dart @@ -6,11 +6,12 @@ part of 'people.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$getAllPeopleHash() => r'226947af3b09ce62224916543958dd1d5e2ba651'; +String _$getAllPeopleHash() => r'2c5e6a207683f15ab209650615fdf9cb7f76c736'; /// See also [getAllPeople]. @ProviderFor(getAllPeople) -final getAllPeopleProvider = AutoDisposeFutureProvider>.internal( +final getAllPeopleProvider = + AutoDisposeFutureProvider>.internal( getAllPeople, name: r'getAllPeopleProvider', debugGetCreateSourceHash: @@ -21,7 +22,7 @@ final getAllPeopleProvider = AutoDisposeFutureProvider>.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; +typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; String _$personAssetsHash() => r'c1d35ee0e024bd6915e21bc724be4b458a14bc24'; /// Copied from Dart SDK diff --git a/mobile/lib/providers/search/search_page_state.provider.dart b/mobile/lib/providers/search/search_page_state.provider.dart index d0e3720c0f..4bbceca383 100644 --- a/mobile/lib/providers/search/search_page_state.provider.dart +++ b/mobile/lib/providers/search/search_page_state.provider.dart @@ -3,8 +3,7 @@ import 'package:immich_mobile/models/search/search_curated_content.model.dart'; import 'package:immich_mobile/services/search.service.dart'; -final getPreviewPlacesProvider = - FutureProvider.autoDispose>((ref) async { +final getPreviewPlacesProvider = FutureProvider.autoDispose>((ref) async { final SearchService searchService = ref.watch(searchServiceProvider); final exploreData = await searchService.getExploreData(); @@ -13,8 +12,7 @@ final getPreviewPlacesProvider = return []; } - final locations = - exploreData.firstWhere((data) => data.fieldName == "exifInfo.city").items; + final locations = exploreData.firstWhere((data) => data.fieldName == "exifInfo.city").items; final curatedContent = locations .map( @@ -28,8 +26,7 @@ final getPreviewPlacesProvider = return curatedContent; }); -final getAllPlacesProvider = - FutureProvider.autoDispose>((ref) async { +final getAllPlacesProvider = FutureProvider.autoDispose>((ref) async { final SearchService searchService = ref.watch(searchServiceProvider); final assetPlaces = await searchService.getAllPlaces(); diff --git a/mobile/lib/providers/secure_storage.provider.dart b/mobile/lib/providers/secure_storage.provider.dart index 0194e527e9..39813d1027 100644 --- a/mobile/lib/providers/secure_storage.provider.dart +++ b/mobile/lib/providers/secure_storage.provider.dart @@ -1,7 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -final secureStorageProvider = - StateNotifierProvider((ref) { +final secureStorageProvider = StateNotifierProvider((ref) { return SecureStorageProvider(); }); diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart index a793acb3f6..4a5e65878b 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -1,44 +1,42 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; - -import 'package:immich_mobile/models/server_info/server_info.model.dart'; -import 'package:immich_mobile/services/server_info.service.dart'; import 'package:immich_mobile/models/server_info/server_config.model.dart'; +import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; import 'package:immich_mobile/models/server_info/server_features.model.dart'; +import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/models/server_info/server_version.model.dart'; +import 'package:immich_mobile/services/server_info.service.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; class ServerInfoNotifier extends StateNotifier { ServerInfoNotifier(this._serverInfoService) : super( - ServerInfo( - serverVersion: const ServerVersion( + const ServerInfo( + serverVersion: ServerVersion( major: 0, minor: 0, patch: 0, ), - latestVersion: const ServerVersion( + latestVersion: ServerVersion( major: 0, minor: 0, patch: 0, ), - serverFeatures: const ServerFeatures( + serverFeatures: ServerFeatures( map: true, trash: true, oauthEnabled: false, passwordLogin: true, ), - serverConfig: const ServerConfig( + serverConfig: ServerConfig( trashDays: 30, oauthButtonText: '', externalDomain: '', - mapLightStyleUrl: - 'https://tiles.immich.cloud/v1/style/light.json', + mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json', mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', ), - serverDiskInfo: const ServerDiskInfo( + serverDiskInfo: ServerDiskInfo( diskAvailable: "0", diskSize: "0", diskUse: "0", @@ -91,8 +89,7 @@ class ServerInfoNotifier extends StateNotifier { if (appVersion["major"]! > serverVersion.major) { state = state.copyWith( isVersionMismatch: true, - versionMismatchErrorMessage: - "profile_drawer_server_out_of_date_major".tr(), + versionMismatchErrorMessage: "profile_drawer_server_out_of_date_major".tr(), ); return; } @@ -100,8 +97,7 @@ class ServerInfoNotifier extends StateNotifier { if (appVersion["major"]! < serverVersion.major) { state = state.copyWith( isVersionMismatch: true, - versionMismatchErrorMessage: - "profile_drawer_client_out_of_date_major".tr(), + versionMismatchErrorMessage: "profile_drawer_client_out_of_date_major".tr(), ); return; } @@ -109,8 +105,7 @@ class ServerInfoNotifier extends StateNotifier { if (appVersion["minor"]! > serverVersion.minor) { state = state.copyWith( isVersionMismatch: true, - versionMismatchErrorMessage: - "profile_drawer_server_out_of_date_minor".tr(), + versionMismatchErrorMessage: "profile_drawer_server_out_of_date_minor".tr(), ); return; } @@ -118,8 +113,7 @@ class ServerInfoNotifier extends StateNotifier { if (appVersion["minor"]! < serverVersion.minor) { state = state.copyWith( isVersionMismatch: true, - versionMismatchErrorMessage: - "profile_drawer_client_out_of_date_minor".tr(), + versionMismatchErrorMessage: "profile_drawer_client_out_of_date_minor".tr(), ); return; } @@ -180,7 +174,6 @@ class ServerInfoNotifier extends StateNotifier { } } -final serverInfoProvider = - StateNotifierProvider((ref) { +final serverInfoProvider = StateNotifierProvider((ref) { return ServerInfoNotifier(ref.read(serverInfoServiceProvider)); }); diff --git a/mobile/lib/providers/shared_link.provider.dart b/mobile/lib/providers/shared_link.provider.dart index 29b628c765..32dfed51f2 100644 --- a/mobile/lib/providers/shared_link.provider.dart +++ b/mobile/lib/providers/shared_link.provider.dart @@ -20,9 +20,7 @@ class SharedLinksNotifier extends StateNotifier>> { } } -final sharedLinksStateProvider = - StateNotifierProvider>>( - (ref) { +final sharedLinksStateProvider = StateNotifierProvider>>((ref) { return SharedLinksNotifier( ref.watch(sharedLinkServiceProvider), ); diff --git a/mobile/lib/providers/sync_status.provider.dart b/mobile/lib/providers/sync_status.provider.dart new file mode 100644 index 0000000000..bf535f525d --- /dev/null +++ b/mobile/lib/providers/sync_status.provider.dart @@ -0,0 +1,130 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +enum SyncStatus { + idle, + syncing, + success, + error; + + localized() { + return switch (this) { + SyncStatus.idle => "idle".tr(), + SyncStatus.syncing => "running".tr(), + SyncStatus.success => "success".tr(), + SyncStatus.error => "error".tr() + }; + } +} + +class SyncStatusState { + final SyncStatus remoteSyncStatus; + final SyncStatus localSyncStatus; + final SyncStatus hashJobStatus; + + final String? errorMessage; + + const SyncStatusState({ + this.remoteSyncStatus = SyncStatus.idle, + this.localSyncStatus = SyncStatus.idle, + this.hashJobStatus = SyncStatus.idle, + this.errorMessage, + }); + + SyncStatusState copyWith({ + SyncStatus? remoteSyncStatus, + SyncStatus? localSyncStatus, + SyncStatus? hashJobStatus, + String? errorMessage, + }) { + return SyncStatusState( + remoteSyncStatus: remoteSyncStatus ?? this.remoteSyncStatus, + localSyncStatus: localSyncStatus ?? this.localSyncStatus, + hashJobStatus: hashJobStatus ?? this.hashJobStatus, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + bool get isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing; + bool get isLocalSyncing => localSyncStatus == SyncStatus.syncing; + bool get isHashing => hashJobStatus == SyncStatus.syncing; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is SyncStatusState && + other.remoteSyncStatus == remoteSyncStatus && + other.localSyncStatus == localSyncStatus && + other.hashJobStatus == hashJobStatus && + other.errorMessage == errorMessage; + } + + @override + int get hashCode => Object.hash( + remoteSyncStatus, + localSyncStatus, + hashJobStatus, + errorMessage, + ); +} + +class SyncStatusNotifier extends Notifier { + @override + SyncStatusState build() { + return const SyncStatusState( + errorMessage: null, + remoteSyncStatus: SyncStatus.idle, + localSyncStatus: SyncStatus.idle, + hashJobStatus: SyncStatus.idle, + ); + } + + /// + /// Remote Sync + /// + + void setRemoteSyncStatus(SyncStatus status, [String? errorMessage]) { + state = state.copyWith( + remoteSyncStatus: status, + errorMessage: status == SyncStatus.error ? errorMessage : null, + ); + } + + void startRemoteSync() => setRemoteSyncStatus(SyncStatus.syncing); + void completeRemoteSync() => setRemoteSyncStatus(SyncStatus.success); + void errorRemoteSync(String error) => setRemoteSyncStatus(SyncStatus.error, error); + + /// + /// Local Sync + /// + + void setLocalSyncStatus(SyncStatus status, [String? errorMessage]) { + state = state.copyWith( + localSyncStatus: status, + errorMessage: status == SyncStatus.error ? errorMessage : null, + ); + } + + void startLocalSync() => setLocalSyncStatus(SyncStatus.syncing); + void completeLocalSync() => setLocalSyncStatus(SyncStatus.success); + void errorLocalSync(String error) => setLocalSyncStatus(SyncStatus.error, error); + + /// + /// Hash Job + /// + + void setHashJobStatus(SyncStatus status, [String? errorMessage]) { + state = state.copyWith( + hashJobStatus: status, + errorMessage: status == SyncStatus.error ? errorMessage : null, + ); + } + + void startHashJob() => setHashJobStatus(SyncStatus.syncing); + void completeHashJob() => setHashJobStatus(SyncStatus.success); + void errorHashJob(String error) => setHashJobStatus(SyncStatus.error, error); +} + +final syncStatusProvider = NotifierProvider( + SyncStatusNotifier.new, +); diff --git a/mobile/lib/providers/theme.provider.dart b/mobile/lib/providers/theme.provider.dart index 73623bd026..bdf3735f8e 100644 --- a/mobile/lib/providers/theme.provider.dart +++ b/mobile/lib/providers/theme.provider.dart @@ -9,9 +9,7 @@ import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; final immichThemeModeProvider = StateProvider((ref) { - final themeMode = ref - .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.themeMode); + final themeMode = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.themeMode); debugPrint("Current themeMode $themeMode"); @@ -26,14 +24,12 @@ final immichThemeModeProvider = StateProvider((ref) { final immichThemePresetProvider = StateProvider((ref) { final appSettingsProvider = ref.watch(appSettingsServiceProvider); - final primaryColorPreset = - appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); + final primaryColorPreset = appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); debugPrint("Current theme preset $primaryColorPreset"); try { - return ImmichColorPreset.values - .firstWhere((e) => e.name == primaryColorPreset); + return ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorPreset); } catch (e) { debugPrint( "Theme preset $primaryColorPreset not found. Applying default preset.", @@ -47,15 +43,11 @@ final immichThemePresetProvider = StateProvider((ref) { }); final dynamicThemeSettingProvider = StateProvider((ref) { - return ref - .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.dynamicTheme); + return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.dynamicTheme); }); final colorfulInterfaceSettingProvider = StateProvider((ref) { - return ref - .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.colorfulInterface); + return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.colorfulInterface); }); // Provider for current selected theme @@ -64,11 +56,7 @@ final immichThemeProvider = StateProvider((ref) { final useSystemColor = ref.watch(dynamicThemeSettingProvider); final useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider); final ImmichTheme? dynamicTheme = DynamicTheme.theme; - final currentTheme = (useSystemColor && dynamicTheme != null) - ? dynamicTheme - : primaryColorPreset.themeOfPreset; + final currentTheme = (useSystemColor && dynamicTheme != null) ? dynamicTheme : primaryColorPreset.themeOfPreset; - return useColorfulInterface - ? currentTheme - : decolorizeSurfaces(theme: currentTheme); + return useColorfulInterface ? currentTheme : decolorizeSurfaces(theme: currentTheme); }); diff --git a/mobile/lib/providers/timeline.provider.dart b/mobile/lib/providers/timeline.provider.dart index b2c763cdfa..6faccff030 100644 --- a/mobile/lib/providers/timeline.provider.dart +++ b/mobile/lib/providers/timeline.provider.dart @@ -18,8 +18,7 @@ final singleUserTimelineProvider = StreamProvider.family( dependencies: [localeProvider], ); -final multiUsersTimelineProvider = - StreamProvider.family>( +final multiUsersTimelineProvider = StreamProvider.family>( (ref, userIds) { ref.watch(localeProvider); final timelineService = ref.watch(timelineServiceProvider); @@ -28,8 +27,7 @@ final multiUsersTimelineProvider = dependencies: [localeProvider], ); -final albumTimelineProvider = - StreamProvider.autoDispose.family((ref, id) { +final albumTimelineProvider = StreamProvider.autoDispose.family((ref, id) { final album = ref.watch(albumWatcher(id)).value; final timelineService = ref.watch(timelineServiceProvider); @@ -65,8 +63,7 @@ final assetSelectionTimelineProvider = StreamProvider((ref) { return timelineService.watchAssetSelectionTimeline(); }); -final assetsTimelineProvider = - FutureProvider.family>((ref, assets) { +final assetsTimelineProvider = FutureProvider.family>((ref, assets) { final timelineService = ref.watch(timelineServiceProvider); return timelineService.getTimelineFromAssets( assets, diff --git a/mobile/lib/providers/timeline/multiselect.provider.dart b/mobile/lib/providers/timeline/multiselect.provider.dart new file mode 100644 index 0000000000..d980ad22b5 --- /dev/null +++ b/mobile/lib/providers/timeline/multiselect.provider.dart @@ -0,0 +1,183 @@ +import 'package:collection/collection.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; + +final multiSelectProvider = NotifierProvider( + MultiSelectNotifier.new, + dependencies: [timelineServiceProvider], +); + +class MultiSelectState { + final Set selectedAssets; + final Set lockedSelectionAssets; + final bool forceEnable; + + const MultiSelectState({ + required this.selectedAssets, + required this.lockedSelectionAssets, + this.forceEnable = false, + }); + + bool get isEnabled => selectedAssets.isNotEmpty; + + /// Cloud only + bool get hasRemote => selectedAssets.any( + (asset) => asset.storage == AssetState.remote || asset.storage == AssetState.merged, + ); + + bool get hasLocal => selectedAssets.any( + (asset) => asset.storage == AssetState.local, + ); + + bool get hasMerged => selectedAssets.any( + (asset) => asset.storage == AssetState.merged, + ); + + MultiSelectState copyWith({ + Set? selectedAssets, + Set? lockedSelectionAssets, + bool? forceEnable, + }) { + return MultiSelectState( + selectedAssets: selectedAssets ?? this.selectedAssets, + lockedSelectionAssets: lockedSelectionAssets ?? this.lockedSelectionAssets, + forceEnable: forceEnable ?? this.forceEnable, + ); + } + + @override + String toString() => + 'MultiSelectState(selectedAssets: $selectedAssets, lockedSelectionAssets: $lockedSelectionAssets, forceEnable: $forceEnable)'; + + @override + bool operator ==(covariant MultiSelectState other) { + if (identical(this, other)) return true; + final setEquals = const DeepCollectionEquality().equals; + + return setEquals(other.selectedAssets, selectedAssets) && + setEquals(other.lockedSelectionAssets, lockedSelectionAssets) && + other.forceEnable == forceEnable; + } + + @override + int get hashCode => selectedAssets.hashCode ^ lockedSelectionAssets.hashCode ^ forceEnable.hashCode; +} + +class MultiSelectNotifier extends Notifier { + MultiSelectNotifier([this._defaultState]); + final MultiSelectState? _defaultState; + + TimelineService get _timelineService => ref.read(timelineServiceProvider); + + @override + MultiSelectState build() { + return _defaultState ?? + const MultiSelectState( + selectedAssets: {}, + lockedSelectionAssets: {}, + forceEnable: false, + ); + } + + void selectAsset(BaseAsset asset) { + if (state.selectedAssets.contains(asset)) { + return; + } + + state = state.copyWith( + selectedAssets: {...state.selectedAssets, asset}, + ); + } + + void deselectAsset(BaseAsset asset) { + if (!state.selectedAssets.contains(asset)) { + return; + } + + state = state.copyWith( + selectedAssets: state.selectedAssets.where((a) => a != asset).toSet(), + ); + } + + void toggleAssetSelection(BaseAsset asset) { + if (state.selectedAssets.contains(asset)) { + deselectAsset(asset); + } else { + selectAsset(asset); + } + } + + void reset() { + state = const MultiSelectState( + selectedAssets: {}, + lockedSelectionAssets: {}, + forceEnable: false, + ); + } + + /// Bucket bulk operations + void selectBucket(int offset, int bucketCount) async { + final assets = await _timelineService.loadAssets(offset, bucketCount); + final selectedAssets = state.selectedAssets.toSet(); + + selectedAssets.addAll(assets); + + state = state.copyWith( + selectedAssets: selectedAssets, + ); + } + + void deselectBucket(int offset, int bucketCount) async { + final assets = await _timelineService.loadAssets(offset, bucketCount); + final selectedAssets = state.selectedAssets.toSet(); + + selectedAssets.removeAll(assets); + + state = state.copyWith(selectedAssets: selectedAssets); + } + + void toggleBucketSelection(int offset, int bucketCount) async { + final assets = await _timelineService.loadAssets(offset, bucketCount); + toggleBucketSelectionByAssets(assets); + } + + void toggleBucketSelectionByAssets(List bucketAssets) { + if (bucketAssets.isEmpty) return; + + // Check if all assets in this bucket are currently selected + final allSelected = bucketAssets.every((asset) => state.selectedAssets.contains(asset)); + + final selectedAssets = state.selectedAssets.toSet(); + + if (allSelected) { + // If all assets in this bucket are selected, deselect them + selectedAssets.removeAll(bucketAssets); + } else { + // If not all assets in this bucket are selected, select them all + selectedAssets.addAll(bucketAssets); + } + + state = state.copyWith(selectedAssets: selectedAssets); + } + + void setLockedSelectionAssets(Set assets) { + state = state.copyWith( + lockedSelectionAssets: assets, + ); + } +} + +final bucketSelectionProvider = Provider.family>( + (ref, bucketAssets) { + final selectedAssets = ref.watch(multiSelectProvider.select((s) => s.selectedAssets)); + + if (bucketAssets.isEmpty) return false; + + // Check if all assets in the bucket are selected + return bucketAssets.every((asset) => selectedAssets.contains(asset)); + }, + dependencies: [multiSelectProvider, timelineServiceProvider], +); diff --git a/mobile/lib/providers/upload_profile_image.provider.dart b/mobile/lib/providers/upload_profile_image.provider.dart index 10aa645654..0588b31b68 100644 --- a/mobile/lib/providers/upload_profile_image.provider.dart +++ b/mobile/lib/providers/upload_profile_image.provider.dart @@ -17,7 +17,7 @@ class UploadProfileImageState { // enum final UploadProfileStatus status; final String profileImagePath; - UploadProfileImageState({ + const UploadProfileImageState({ required this.status, required this.profileImagePath, }); @@ -50,31 +50,26 @@ class UploadProfileImageState { String toJson() => json.encode(toMap()); - factory UploadProfileImageState.fromJson(String source) => - UploadProfileImageState.fromMap(json.decode(source)); + factory UploadProfileImageState.fromJson(String source) => UploadProfileImageState.fromMap(json.decode(source)); @override - String toString() => - 'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)'; + String toString() => 'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)'; @override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is UploadProfileImageState && - other.status == status && - other.profileImagePath == profileImagePath; + return other is UploadProfileImageState && other.status == status && other.profileImagePath == profileImagePath; } @override int get hashCode => status.hashCode ^ profileImagePath.hashCode; } -class UploadProfileImageNotifier - extends StateNotifier { +class UploadProfileImageNotifier extends StateNotifier { UploadProfileImageNotifier(this._userService) : super( - UploadProfileImageState( + const UploadProfileImageState( profileImagePath: '', status: UploadProfileStatus.idle, ), @@ -104,7 +99,6 @@ class UploadProfileImageNotifier } } -final uploadProfileImageProvider = - StateNotifierProvider( +final uploadProfileImageProvider = StateNotifierProvider( ((ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider))), ); diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index 1a1c21554c..10dcb2aff5 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -10,8 +10,7 @@ import 'package:immich_mobile/services/timeline.service.dart'; class CurrentUserProvider extends StateNotifier { CurrentUserProvider(this._userService) : super(null) { state = _userService.tryGetMyUser(); - streamSub = - _userService.watchMyUser().listen((user) => state = user ?? state); + streamSub = _userService.watchMyUser().listen((user) => state = user ?? state); } final UserService _userService; @@ -30,8 +29,7 @@ class CurrentUserProvider extends StateNotifier { } } -final currentUserProvider = - StateNotifierProvider((ref) { +final currentUserProvider = StateNotifierProvider((ref) { return CurrentUserProvider(ref.watch(userServiceProvider)); }); @@ -56,7 +54,6 @@ class TimelineUserIdsProvider extends StateNotifier> { } } -final timelineUsersIdsProvider = - StateNotifierProvider>((ref) { +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 72dbda8b6f..6c24cc0568 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; @@ -10,6 +11,8 @@ import 'package:immich_mobile/entities/store.entity.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/background_sync.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; @@ -56,7 +59,7 @@ class WebsocketState { final bool isConnected; final List pendingChanges; - WebsocketState({ + const WebsocketState({ this.socket, required this.isConnected, required this.pendingChanges, @@ -75,16 +78,13 @@ class WebsocketState { } @override - String toString() => - 'WebsocketState(socket: $socket, isConnected: $isConnected)'; + String toString() => 'WebsocketState(socket: $socket, isConnected: $isConnected)'; @override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is WebsocketState && - other.socket == socket && - other.isConnected == isConnected; + return other is WebsocketState && other.socket == socket && other.isConnected == isConnected; } @override @@ -94,13 +94,28 @@ class WebsocketState { class WebsocketNotifier extends StateNotifier { WebsocketNotifier(this._ref) : super( - WebsocketState(socket: null, isConnected: false, pendingChanges: []), + const WebsocketState( + socket: null, + isConnected: false, + pendingChanges: [], + ), ); final _log = Logger('WebsocketNotifier'); final Ref _ref; - final Debouncer _debounce = - Debouncer(interval: const Duration(milliseconds: 500)); + final Debouncer _debounce = Debouncer(interval: const Duration(milliseconds: 500)); + + final Debouncer _batchDebouncer = Debouncer( + interval: const Duration(seconds: 5), + maxWaitTime: const Duration(seconds: 10), + ); + final List _batchedAssetUploadReady = []; + + @override + void dispose() { + _batchDebouncer.dispose(); + super.dispose(); + } /// Connects websocket to server unless already connected void connect() { @@ -112,8 +127,7 @@ class WebsocketNotifier extends StateNotifier { final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint)); final headers = ApiService.getRequestHeaders(); if (endpoint.userInfo.isNotEmpty) { - headers["Authorization"] = - "Basic ${base64.encode(utf8.encode(endpoint.userInfo))}"; + headers["Authorization"] = "Basic ${base64.encode(utf8.encode(endpoint.userInfo))}"; } debugPrint("Attempting to connect to websocket"); @@ -158,14 +172,19 @@ class WebsocketNotifier extends StateNotifier { ); }); - socket.on('on_upload_success', _handleOnUploadSuccess); + 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('on_config_update', _handleOnConfigUpdate); - 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); socket.on('on_new_release', _handleReleaseUpdates); } catch (e) { debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); @@ -176,6 +195,8 @@ class WebsocketNotifier extends StateNotifier { void disconnect() { debugPrint("Attempting to disconnect from websocket"); + _batchedAssetUploadReady.clear(); + var socket = state.socket?.disconnect(); if (socket?.disconnected == true) { @@ -188,10 +209,37 @@ class WebsocketNotifier extends StateNotifier { } void stopListenToEvent(String eventName) { - debugPrint("Stop listening to event $eventName"); state.socket?.off(eventName); } + 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'); + } + + void startListeningToBetaEvents() { + state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); + } + void listenUploadEvent() { debugPrint("Start listening to event on_upload_success"); state.socket?.on('on_upload_success', _handleOnUploadSuccess); @@ -209,49 +257,34 @@ class WebsocketNotifier extends StateNotifier { } Future _handlePendingTrashes() async { - final trashChanges = state.pendingChanges - .where((c) => c.action == PendingAction.assetTrash) - .toList(); + 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(); + 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(), + pendingChanges: state.pendingChanges.whereNot((c) => trashChanges.contains(c)).toList(), ); } } Future _handlePendingDeletes() async { - final deleteChanges = state.pendingChanges - .where((c) => c.action == PendingAction.assetDelete) - .toList(); + final deleteChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetDelete).toList(); if (deleteChanges.isNotEmpty) { - List remoteIds = - deleteChanges.map((a) => a.value.toString()).toList(); + 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(), + pendingChanges: state.pendingChanges.whereNot((c) => deleteChanges.contains(c)).toList(), ); } } Future _handlePendingUploaded() async { - final uploadedChanges = state.pendingChanges - .where((c) => c.action == PendingAction.assetUploaded) - .toList(); + final uploadedChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetUploaded).toList(); if (uploadedChanges.isNotEmpty) { - List remoteAssets = uploadedChanges - .map((a) => AssetResponseDto.fromJson(a.value)) - .toList(); + List remoteAssets = uploadedChanges.map((a) => AssetResponseDto.fromJson(a.value)).toList(); for (final dto in remoteAssets) { if (dto != null) { final newAsset = Asset.remote(dto); @@ -259,32 +292,25 @@ class WebsocketNotifier extends StateNotifier { } } state = state.copyWith( - pendingChanges: state.pendingChanges - .whereNot((c) => uploadedChanges.contains(c)) - .toList(), + pendingChanges: state.pendingChanges.whereNot((c) => uploadedChanges.contains(c)).toList(), ); } } Future _handlingPendingHidden() async { - final hiddenChanges = state.pendingChanges - .where((c) => c.action == PendingAction.assetHidden) - .toList(); + final hiddenChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetHidden).toList(); if (hiddenChanges.isNotEmpty) { - List remoteIds = - hiddenChanges.map((a) => a.value.toString()).toList(); + List remoteIds = hiddenChanges.map((a) => a.value.toString()).toList(); final db = _ref.watch(dbProvider); await db.writeTxn(() => db.assets.deleteAllByRemoteId(remoteIds)); state = state.copyWith( - pendingChanges: state.pendingChanges - .whereNot((c) => hiddenChanges.contains(c)) - .toList(), + pendingChanges: state.pendingChanges.whereNot((c) => hiddenChanges.contains(c)).toList(), ); } } - void handlePendingChanges() async { + Future handlePendingChanges() async { await _handlePendingUploaded(); await _handlePendingDeletes(); await _handlingPendingHidden(); @@ -301,18 +327,15 @@ class WebsocketNotifier extends StateNotifier { _ref.read(assetProvider.notifier).getAllAsset(); } - void _handleOnUploadSuccess(dynamic data) => - addPendingChange(PendingAction.assetUploaded, data); + void _handleOnUploadSuccess(dynamic data) => addPendingChange(PendingAction.assetUploaded, data); - void _handleOnAssetDelete(dynamic data) => - addPendingChange(PendingAction.assetDelete, 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); + void _handleOnAssetHidden(dynamic data) => addPendingChange(PendingAction.assetHidden, data); _handleReleaseUpdates(dynamic data) { // Json guard @@ -321,31 +344,45 @@ class WebsocketNotifier extends StateNotifier { } final json = data.cast(); - final serverVersionJson = - json.containsKey('serverVersion') ? json['serverVersion'] : null; - final releaseVersionJson = - json.containsKey('releaseVersion') ? json['releaseVersion'] : null; + final serverVersionJson = json.containsKey('serverVersion') ? json['serverVersion'] : null; + final releaseVersionJson = json.containsKey('releaseVersion') ? json['releaseVersion'] : null; if (serverVersionJson == null || releaseVersionJson == null) { return; } - final serverVersionDto = - ServerVersionResponseDto.fromJson(serverVersionJson); - final releaseVersionDto = - ServerVersionResponseDto.fromJson(releaseVersionJson); + final serverVersionDto = ServerVersionResponseDto.fromJson(serverVersionJson); + final releaseVersionDto = ServerVersionResponseDto.fromJson(releaseVersionJson); if (serverVersionDto == null || releaseVersionDto == null) { return; } final serverVersion = ServerVersion.fromDto(serverVersionDto); final releaseVersion = ServerVersion.fromDto(releaseVersionDto); - _ref - .read(serverInfoProvider.notifier) - .handleNewRelease(serverVersion, releaseVersion); + _ref.read(serverInfoProvider.notifier).handleNewRelease(serverVersion, releaseVersion); + } + + void _handleSyncAssetUploadReady(dynamic data) { + _batchedAssetUploadReady.add(data); + _batchDebouncer.run(_processBatchedAssetUploadReady); + } + + void _processBatchedAssetUploadReady() { + if (_batchedAssetUploadReady.isEmpty) { + return; + } + + try { + unawaited( + _ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()), + ); + } catch (error) { + _log.severe("Error processing batched AssetUploadReadyV1 events: $error"); + } + + _batchedAssetUploadReady.clear(); } } -final websocketProvider = - StateNotifierProvider((ref) { +final websocketProvider = StateNotifierProvider((ref) { return WebsocketNotifier(ref); }); diff --git a/mobile/lib/repositories/activity_api.repository.dart b/mobile/lib/repositories/activity_api.repository.dart index 1ee92b2e2f..36f380cba7 100644 --- a/mobile/lib/repositories/activity_api.repository.dart +++ b/mobile/lib/repositories/activity_api.repository.dart @@ -15,8 +15,7 @@ class ActivityApiRepository extends ApiRepository { ActivityApiRepository(this._api); Future> getAll(String albumId, {String? assetId}) async { - final response = - await checkNull(_api.getActivities(albumId, assetId: assetId)); + final response = await checkNull(_api.getActivities(albumId, assetId: assetId)); return response.map(_toActivity).toList(); } @@ -28,9 +27,7 @@ class ActivityApiRepository extends ApiRepository { }) async { final dto = ActivityCreateDto( albumId: albumId, - type: type == ActivityType.comment - ? ReactionType.comment - : ReactionType.like, + type: type == ActivityType.comment ? ReactionType.comment : ReactionType.like, assetId: assetId, comment: comment, ); @@ -43,17 +40,14 @@ class ActivityApiRepository extends ApiRepository { } Future getStats(String albumId, {String? assetId}) async { - final response = - await checkNull(_api.getActivityStatistics(albumId, assetId: assetId)); + final response = await checkNull(_api.getActivityStatistics(albumId, assetId: assetId)); return ActivityStats(comments: response.comments); } static Activity _toActivity(ActivityResponseDto dto) => Activity( id: dto.id, createdAt: dto.createdAt, - type: dto.type == ReactionType.comment - ? ActivityType.comment - : ActivityType.like, + type: dto.type == ReactionType.comment ? ActivityType.comment : ActivityType.like, user: UserConverter.fromSimpleUserDto(dto.user), assetId: dto.assetId, comment: dto.comment, diff --git a/mobile/lib/repositories/album.repository.dart b/mobile/lib/repositories/album.repository.dart index 8c50c54382..c65dce325d 100644 --- a/mobile/lib/repositories/album.repository.dart +++ b/mobile/lib/repositories/album.repository.dart @@ -4,22 +4,20 @@ 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/interfaces/album.interface.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'; -final albumRepositoryProvider = - Provider((ref) => AlbumRepository(ref.watch(dbProvider))); +enum AlbumSort { remoteId, localId } -class AlbumRepository extends DatabaseRepository implements IAlbumRepository { - AlbumRepository(super.db); +final albumRepositoryProvider = Provider((ref) => AlbumRepository(ref.watch(dbProvider))); + +class AlbumRepository extends DatabaseRepository { + const AlbumRepository(super.db); - @override Future count({bool? local}) { final baseQuery = db.albums.where(); final QueryBuilder query = switch (local) { @@ -30,10 +28,8 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository { return query.count(); } - @override Future create(Album album) => txn(() => db.albums.store(album)); - @override Future getByName( String name, { bool? shared, @@ -58,13 +54,10 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository { return query.findFirst(); } - @override Future update(Album album) => txn(() => db.albums.store(album)); - @override Future delete(int albumId) => txn(() => db.albums.delete(albumId)); - @override Future> getAll({ bool? shared, bool? remote, @@ -80,8 +73,7 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository { } else { afterWhere = baseQuery.localIdIsNotNull(); } - QueryBuilder filterQuery = - afterWhere.filter().noOp(); + QueryBuilder filterQuery = afterWhere.filter().noOp(); if (shared != null) { filterQuery = filterQuery.sharedEqualTo(true); } @@ -96,48 +88,37 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository { return query.findAll(); } - @override Future get(int id) => db.albums.get(id); - @override + 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)), ); - @override - Future addAssets(Album album, List assets) => - txn(() => album.assets.update(link: assets)); + Future addAssets(Album album, List assets) => txn(() => album.assets.update(link: assets)); - @override - Future removeAssets(Album album, List assets) => - txn(() => album.assets.update(unlink: assets)); + Future removeAssets(Album album, List assets) => txn(() => album.assets.update(unlink: assets)); - @override 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(); + album.lastModifiedAssetTimestamp = await album.assets.filter().updatedAtProperty().max(); return album; } - @override Future addUsers(Album album, List users) => txn(() => album.sharedUsers.update(link: users.map(entity.User.fromDto))); - @override - Future deleteAllLocal() => - txn(() => db.albums.where().localIdIsNotNull().deleteAll()); + Future deleteAllLocal() => txn(() => db.albums.where().localIdIsNotNull().deleteAll()); - @override Future> search( String searchTerm, QuickFilterMode filterMode, ) async { - var query = db.albums - .filter() - .nameContains(searchTerm, caseSensitive: false) - .remoteIdIsNotNull(); + var query = db.albums.filter().nameContains(searchTerm, caseSensitive: false).remoteIdIsNotNull(); final isarUserId = fastHash(Store.get(StoreKey.currentUser).id); switch (filterMode) { @@ -152,24 +133,20 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository { return await query.findAll(); } - @override Future clearTable() async { await txn(() async { await db.albums.clear(); }); } - @override Stream> watchRemoteAlbums() { return db.albums.where().remoteIdIsNotNull().watch(); } - @override Stream> watchLocalAlbums() { return db.albums.where().localIdIsNotNull().watch(); } - @override 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 index 019e4dc63c..b5b7c72883 100644 --- a/mobile/lib/repositories/album_api.repository.dart +++ b/mobile/lib/repositories/album_api.repository.dart @@ -1,9 +1,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart' 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/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'; @@ -50,6 +50,25 @@ class AlbumApiRepository extends ApiRepository { 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, @@ -129,8 +148,7 @@ class AlbumApiRepository extends ApiRepository { } Future addUsers(String albumId, Iterable userIds) async { - final albumUsers = - userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList(); + final albumUsers = userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList(); final response = await checkNull( _api.addUsersToAlbum( albumId, @@ -159,15 +177,29 @@ class AlbumApiRepository extends ApiRepository { sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc, ); album.remoteAssetCount = dto.assetCount; - album.owner.value = - entity.User.fromDto(UserConverter.fromSimpleUserDto(dto.owner)); + album.owner.value = entity.User.fromDto(UserConverter.fromSimpleUserDto(dto.owner)); album.remoteThumbnailAssetId = dto.albumThumbnailAssetId; - final users = dto.albumUsers - .map((albumUser) => UserConverter.fromSimpleUserDto(albumUser.user)); + 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, + ); + } } diff --git a/mobile/lib/repositories/album_media.repository.dart b/mobile/lib/repositories/album_media.repository.dart index b3d31f4a1b..6e9dda173c 100644 --- a/mobile/lib/repositories/album_media.repository.dart +++ b/mobile/lib/repositories/album_media.repository.dart @@ -7,14 +7,12 @@ 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()); +final albumMediaRepositoryProvider = Provider((ref) => const AlbumMediaRepository()); class AlbumMediaRepository { const AlbumMediaRepository(); - bool get useCustomFilter => - Store.get(StoreKey.photoManagerCustomFilter, false); + bool get useCustomFilter => Store.get(StoreKey.photoManagerCustomFilter, true); FilterOptionGroup? _getAlbumFilter({ DateTimeCond? updateTimeCond, @@ -34,8 +32,7 @@ class AlbumMediaRepository { ), containsPathModified: containsPathModified ?? false, createTimeCond: DateTimeCond.def().copyWith(ignore: true), - updateTimeCond: - updateTimeCond ?? DateTimeCond.def().copyWith(ignore: true), + updateTimeCond: updateTimeCond ?? DateTimeCond.def().copyWith(ignore: true), orders: orderBy ?? [], ) : null; @@ -51,16 +48,13 @@ class AlbumMediaRepository { } Future> getAssetIds(String albumId) async { - final album = - await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); - final List assets = - await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff); + 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()); + final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); return album.assetCountAsync; } @@ -81,14 +75,11 @@ class AlbumMediaRepository { min: modifiedFrom ?? DateTime.utc(-271820), max: modifiedUntil ?? DateTime.utc(275760), ), - orderBy: orderByModificationDate - ? [const OrderOption(type: OrderOptionType.updateDate)] - : [], + orderBy: orderByModificationDate ? [const OrderOption(type: OrderOptionType.updateDate)] : [], ), ); - final List assets = - await onDevice.getAssetListRange(start: start, end: end); + final List assets = await onDevice.getAssetListRange(start: start, end: end); return assets.map(AssetMediaRepository.toAsset).toList().cast(); } @@ -103,10 +94,8 @@ class AlbumMediaRepository { 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(), + createdAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(), + modifiedAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(), shared: false, activityEnabled: false, ); diff --git a/mobile/lib/repositories/api.repository.dart b/mobile/lib/repositories/api.repository.dart index b454c77f9b..646e2480e9 100644 --- a/mobile/lib/repositories/api.repository.dart +++ b/mobile/lib/repositories/api.repository.dart @@ -3,7 +3,7 @@ import 'package:immich_mobile/constants/errors.dart'; abstract class ApiRepository { Future checkNull(Future future) async { final response = await future; - if (response == null) throw NoResponseDtoError(); + if (response == null) throw const NoResponseDtoError(); return response; } } diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart index c6f8539167..2b35364596 100644 --- a/mobile/lib/repositories/asset.repository.dart +++ b/mobile/lib/repositories/asset.repository.dart @@ -4,19 +4,18 @@ 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/interfaces/asset.interface.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'; -final assetRepositoryProvider = - Provider((ref) => AssetRepository(ref.watch(dbProvider))); +enum AssetSort { checksum, ownerIdChecksum } -class AssetRepository extends DatabaseRepository implements IAssetRepository { - AssetRepository(super.db); +final assetRepositoryProvider = Provider((ref) => AssetRepository(ref.watch(dbProvider))); + +class AssetRepository extends DatabaseRepository { + const AssetRepository(super.db); - @override Future> getByAlbum( Album album, { Iterable notOwnedBy = const [], @@ -29,8 +28,7 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { 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)); + query = query.not().anyOf(isarUserIds, (q, int id) => q.ownerIdEqualTo(id)); } if (ownerId != null) { query = query.ownerIdEqualTo(fastHash(ownerId)); @@ -44,8 +42,7 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { }; } - final QueryBuilder sortedQuery = - switch (sortBy) { + final QueryBuilder sortedQuery = switch (sortBy) { null => query.noOp(), AssetSort.checksum => query.sortByChecksum(), AssetSort.ownerIdChecksum => query.sortByOwnerId().thenByChecksum(), @@ -54,16 +51,13 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { return sortedQuery.findAll(); } - @override Future deleteByIds(List ids) => txn(() async { await db.assets.deleteAll(ids); await db.exifInfos.deleteAll(ids); }); - @override Future getByRemoteId(String id) => db.assets.getByRemoteId(id); - @override Future> getAllByRemoteId( Iterable ids, { AssetState? state, @@ -88,7 +82,6 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { }; } - @override Future> getAll({ required String ownerId, AssetState? state, @@ -97,43 +90,28 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { }) { final baseQuery = db.assets.where(); final isarUserIds = fastHash(ownerId); - final QueryBuilder filteredQuery = - switch (state) { + 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(), + 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(), + AssetSort.ownerIdChecksum => filteredQuery.sortByOwnerId().thenByChecksum(), }; return limit == null ? query.findAll() : query.limit(limit).findAll(); } - @override Future> updateAll(List assets) async { await txn(() => db.assets.putAll(assets)); return assets; } - @override Future> getMatches({ required List assets, required String ownerId, @@ -141,55 +119,40 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { int limit = 100, }) { final baseQuery = db.assets.where(); - final QueryBuilder query = - switch (state) { + 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(), + 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); } - @override Future update(Asset asset) async { await txn(() => asset.put(db)); return asset; } - @override Future upsertDuplicatedAssets(Iterable duplicatedAssets) => txn( - () => db.duplicatedAssets - .putAll(duplicatedAssets.map(DuplicatedAsset.new).toList()), + () => db.duplicatedAssets.putAll(duplicatedAssets.map(DuplicatedAsset.new).toList()), ); - @override - Future> getAllDuplicatedAssetIds() => - db.duplicatedAssets.where().idProperty().findAll(); + Future> getAllDuplicatedAssetIds() => db.duplicatedAssets.where().idProperty().findAll(); - @override Future getByOwnerIdChecksum(int ownerId, String checksum) => db.assets.getByOwnerIdChecksum(ownerId, checksum); - @override Future> getAllByOwnerIdChecksum( List ownerIds, List checksums, ) => db.assets.getAllByOwnerIdChecksum(ownerIds, checksums); - @override - Future> getAllLocal() => - db.assets.where().localIdIsNotNull().findAll(); + Future> getAllLocal() => db.assets.where().localIdIsNotNull().findAll(); - @override Future deleteAllByRemoteId(List ids, {AssetState? state}) => txn(() => _getAllByRemoteIdImpl(ids, state).deleteAll()); - @override Future> getStackAssets(String stackId) { return db.assets .filter() @@ -202,19 +165,16 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { .findAll(); } - @override Future clearTable() async { await txn(() async { await db.assets.clear(); }); } - @override Stream watchAsset(int id, {bool fireImmediately = false}) { return db.assets.watchObject(id, fireImmediately: fireImmediately); } - @override Future> getTrashAssets(String userId) { return db.assets .where() @@ -225,7 +185,6 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { .findAll(); } - @override Future> getRecentlyTakenAssets(String userId) { return db.assets .where() @@ -236,7 +195,6 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { .findAll(); } - @override Future> getMotionAssets(String userId) { return db.assets .where() diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index f82df4b774..b85ebdea6b 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -1,25 +1,35 @@ 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/stack.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/interfaces/asset_api.interface.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; 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, ), ); -class AssetApiRepository extends ApiRepository implements IAssetApiRepository { +class AssetApiRepository extends ApiRepository { final AssetsApi _api; final SearchApi _searchApi; + final StacksApi _stacksApi; + final TrashApi _trashApi; - AssetApiRepository(this._api, this._searchApi); + AssetApiRepository( + this._api, + this._searchApi, + this._stacksApi, + this._trashApi, + ); - @override Future update(String id, {String? description}) async { final response = await checkNull( _api.updateAsset(id, UpdateAssetDto(description: description)), @@ -27,7 +37,6 @@ class AssetApiRepository extends ApiRepository implements IAssetApiRepository { return Asset.remote(response); } - @override Future> search({List personIds = const []}) async { // TODO this always fetches all assets, change API and usage to actually do pagination final List result = []; @@ -50,7 +59,14 @@ class AssetApiRepository extends ApiRepository implements IAssetApiRepository { return result; } - @override + Future delete(List ids, bool force) async { + return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: force)); + } + + Future restoreTrash(List ids) async { + await _trashApi.restoreAssets(BulkIdsDto(ids: ids)); + } + Future updateVisibility( List ids, AssetVisibilityEnum visibility, @@ -60,20 +76,49 @@ class AssetApiRepository extends ApiRepository implements IAssetApiRepository { ); } - _mapVisibility(AssetVisibilityEnum visibility) { - switch (visibility) { - case AssetVisibilityEnum.timeline: - return AssetVisibility.timeline; - case AssetVisibilityEnum.hidden: - return AssetVisibility.hidden; - case AssetVisibilityEnum.locked: - return AssetVisibility.locked; - case AssetVisibilityEnum.archive: - return AssetVisibility.archive; - } + Future updateFavorite( + List ids, + bool isFavorite, + ) async { + return _api.updateAssets( + AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite), + ); } - @override + Future updateLocation( + List ids, + LatLng location, + ) async { + return _api.updateAssets( + AssetBulkUpdateDto( + ids: ids, + latitude: location.latitude, + longitude: location.longitude, + ), + ); + } + + Future stack(List ids) async { + final responseDto = await checkNull(_stacksApi.createStack(StackCreateDto(assetIds: ids))); + + return responseDto.toStack(); + } + + Future unStack(List ids) async { + return _stacksApi.deleteStacks(BulkIdsDto(ids: ids)); + } + + Future downloadAsset(String id) { + return _api.downloadAssetWithHttpInfo(id); + } + + _mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) { + AssetVisibilityEnum.timeline => AssetVisibility.timeline, + AssetVisibilityEnum.hidden => AssetVisibility.hidden, + AssetVisibilityEnum.locked => AssetVisibility.locked, + AssetVisibilityEnum.archive => AssetVisibility.archive, + }; + Future getAssetMIMEType(String assetId) async { final response = await checkNull(_api.getAssetInfo(assetId)); @@ -81,3 +126,13 @@ class AssetApiRepository extends ApiRepository implements IAssetApiRepository { return response.originalMimeType; } } + +extension on StackResponseDto { + StackResponse toStack() { + return StackResponse( + id: id, + primaryAssetId: primaryAssetId, + assetIds: assets.map((asset) => asset.id).toList(), + ); + } +} diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index 7df26455cd..1355890766 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -1,28 +1,39 @@ +import 'dart:io'; + 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/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/interfaces/asset_media.interface.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/utils/hash.dart'; -import 'package:photo_manager/photo_manager.dart' hide AssetType; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/response_extensions.dart'; +import 'package:share_plus/share_plus.dart'; -final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository()); +final assetMediaRepositoryProvider = Provider( + (ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider)), +); -class AssetMediaRepository implements IAssetMediaRepository { - @override - Future> deleteAll(List ids) => - PhotoManager.editor.deleteWithIds(ids); +class AssetMediaRepository { + final AssetApiRepository _assetApiRepository; + static final Logger _log = Logger("AssetMediaRepository"); - @override - Future get(String id) async { + const AssetMediaRepository(this._assetApiRepository); + + Future> deleteAll(List ids) => PhotoManager.editor.deleteWithIds(ids); + + Future get(String id) async { final entity = await AssetEntity.fromId(id); return toAsset(entity); } - static Asset? toAsset(AssetEntity? local) { + static asset_entity.Asset? toAsset(AssetEntity? local) { if (local == null) return null; - final Asset asset = Asset( + final asset_entity.Asset asset = asset_entity.Asset( checksum: "", localId: local.id, ownerId: fastHash(Store.get(StoreKey.currentUser).id), @@ -30,7 +41,7 @@ class AssetMediaRepository implements IAssetMediaRepository { fileModifiedAt: local.modifiedDateTime, updatedAt: local.modifiedDateTime, durationInSeconds: local.duration, - type: AssetType.values[local.typeInt], + type: asset_entity.AssetType.values[local.typeInt], fileName: local.title!, width: local.width, height: local.height, @@ -40,14 +51,12 @@ class AssetMediaRepository implements IAssetMediaRepository { asset.fileCreatedAt = asset.fileModifiedAt; } if (local.latitude != null) { - asset.exifInfo = - ExifInfo(latitude: local.latitude, longitude: local.longitude); + asset.exifInfo = ExifInfo(latitude: local.latitude, longitude: local.longitude); } asset.local = local; return asset; } - @override Future getOriginalFilename(String id) async { final entity = await AssetEntity.fromId(id); @@ -59,4 +68,53 @@ class AssetMediaRepository implements IAssetMediaRepository { // otherwise using the `entity.title` would return a random GUID return await entity.titleAsync; } + + // TODO: make this more efficient + Future shareAssets(List assets) async { + final downloadedXFiles = []; + + for (var asset in assets) { + final localId = (asset is LocalAsset) + ? asset.id + : asset is RemoteAsset + ? asset.localId + : null; + if (localId != null) { + File? f = await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0).originFile; + downloadedXFiles.add(XFile(f!.path)); + } else if (asset is RemoteAsset) { + final tempDir = await getTemporaryDirectory(); + final name = asset.name; + final tempFile = await File('${tempDir.path}/$name').create(); + final res = await _assetApiRepository.downloadAsset(asset.id); + + if (res.statusCode != 200) { + _log.severe("Download for $name failed", res.toLoggerString()); + continue; + } + + await tempFile.writeAsBytes(res.bodyBytes); + downloadedXFiles.add(XFile(tempFile.path)); + } else { + _log.warning("Asset type not supported for sharing: $asset"); + continue; + } + } + + if (downloadedXFiles.isEmpty) { + _log.warning("No asset can be retrieved for share"); + return 0; + } + + final result = await Share.shareXFiles(downloadedXFiles); + + for (var file in downloadedXFiles) { + try { + await File(file.path).delete(); + } catch (e) { + _log.warning("Failed to delete temporary file: ${file.path}", e); + } + } + return result.status == ShareResultStatus.success ? downloadedXFiles.length : 0; + } } diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index 01d2684faf..9d7748254d 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -10,24 +10,41 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/interfaces/auth.interface.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/repositories/database.repository.dart'; -final authRepositoryProvider = Provider( - (ref) => - AuthRepository(ref.watch(dbProvider), drift: ref.watch(driftProvider)), +final authRepositoryProvider = Provider( + (ref) => AuthRepository(ref.watch(dbProvider), ref.watch(driftProvider)), ); -class AuthRepository extends DatabaseRepository implements IAuthRepository { +class AuthRepository extends DatabaseRepository { final Drift _drift; - AuthRepository(super.db, {required Drift drift}) : _drift = drift; + const AuthRepository(super.db, this._drift); + + Future clearLocalData() async { + // Drift deletions - child entities first (those with foreign keys) + await Future.wait([ + _drift.memoryAssetEntity.deleteAll(), + _drift.remoteAlbumAssetEntity.deleteAll(), + _drift.remoteAlbumUserEntity.deleteAll(), + _drift.remoteExifEntity.deleteAll(), + _drift.userMetadataEntity.deleteAll(), + _drift.partnerEntity.deleteAll(), + _drift.stackEntity.deleteAll(), + _drift.assetFaceEntity.deleteAll(), + ]); + // Drift deletions - parent entities + await Future.wait([ + _drift.memoryEntity.deleteAll(), + _drift.personEntity.deleteAll(), + _drift.remoteAlbumEntity.deleteAll(), + _drift.remoteAssetEntity.deleteAll(), + _drift.userEntity.deleteAll(), + ]); - @override - Future clearLocalData() { return db.writeTxn(() { return Future.wait([ db.assets.clear(), @@ -35,33 +52,26 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository { db.albums.clear(), db.eTags.clear(), db.users.clear(), - _drift.remoteAssetEntity.deleteAll(), - _drift.remoteExifEntity.deleteAll(), ]); }); } - @override String getAccessToken() { return Store.get(StoreKey.accessToken); } - @override bool getEndpointSwitchingFeature() { return Store.tryGet(StoreKey.autoEndpointSwitching) ?? false; } - @override String? getPreferredWifiName() { return Store.tryGet(StoreKey.preferredWifiName); } - @override String? getLocalEndpoint() { return Store.tryGet(StoreKey.localEndpoint); } - @override List getExternalEndpointList() { final jsonString = Store.tryGet(StoreKey.externalEndpointList); @@ -70,8 +80,7 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository { } final List jsonList = jsonDecode(jsonString); - final endpointList = - jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList(); + final endpointList = jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList(); return endpointList; } diff --git a/mobile/lib/repositories/auth_api.repository.dart b/mobile/lib/repositories/auth_api.repository.dart index 4015ffd7bc..992be918f9 100644 --- a/mobile/lib/repositories/auth_api.repository.dart +++ b/mobile/lib/repositories/auth_api.repository.dart @@ -1,20 +1,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/interfaces/auth_api.interface.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:openapi/api.dart'; -final authApiRepositoryProvider = - Provider((ref) => AuthApiRepository(ref.watch(apiServiceProvider))); +final authApiRepositoryProvider = Provider((ref) => AuthApiRepository(ref.watch(apiServiceProvider))); -class AuthApiRepository extends ApiRepository implements IAuthApiRepository { +class AuthApiRepository extends ApiRepository { final ApiService _apiService; AuthApiRepository(this._apiService); - @override Future changePassword(String newPassword) async { await _apiService.usersApi.updateMyUser( UserUpdateMeDto( @@ -23,7 +20,6 @@ class AuthApiRepository extends ApiRepository implements IAuthApiRepository { ); } - @override Future login(String email, String password) async { final loginResponseDto = await checkNull( _apiService.authenticationApi.login( @@ -37,11 +33,8 @@ class AuthApiRepository extends ApiRepository implements IAuthApiRepository { return _mapLoginReponse(loginResponseDto); } - @override Future logout() async { - await _apiService.authenticationApi - .logout() - .timeout(const Duration(seconds: 7)); + await _apiService.authenticationApi.logout().timeout(const Duration(seconds: 7)); } _mapLoginReponse(LoginResponseDto dto) { @@ -56,24 +49,19 @@ class AuthApiRepository extends ApiRepository implements IAuthApiRepository { ); } - @override Future unlockPinCode(String pinCode) async { try { - await _apiService.authenticationApi - .unlockAuthSession(SessionUnlockDto(pinCode: pinCode)); + await _apiService.authenticationApi.unlockAuthSession(SessionUnlockDto(pinCode: pinCode)); return true; } catch (_) { return false; } } - @override Future setupPinCode(String pinCode) { - return _apiService.authenticationApi - .setupPinCode(PinCodeSetupDto(pinCode: pinCode)); + return _apiService.authenticationApi.setupPinCode(PinCodeSetupDto(pinCode: pinCode)); } - @override Future lockPinCode() { return _apiService.authenticationApi.lockAuthSession(); } diff --git a/mobile/lib/repositories/backup.repository.dart b/mobile/lib/repositories/backup.repository.dart index f7f3051f46..6cee6a4427 100644 --- a/mobile/lib/repositories/backup.repository.dart +++ b/mobile/lib/repositories/backup.repository.dart @@ -1,41 +1,32 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/repositories/database.repository.dart'; import 'package:isar/isar.dart'; -final backupAlbumRepositoryProvider = - Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider))); +enum BackupAlbumSort { id } -class BackupAlbumRepository extends DatabaseRepository - implements IBackupAlbumRepository { - BackupAlbumRepository(super.db); +final backupAlbumRepositoryProvider = Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider))); + +class BackupAlbumRepository extends DatabaseRepository { + const BackupAlbumRepository(super.db); - @override Future> getAll({BackupAlbumSort? sort}) { final baseQuery = db.backupAlbums.where(); - final QueryBuilder query = - switch (sort) { + final QueryBuilder query = switch (sort) { null => baseQuery.noOp(), BackupAlbumSort.id => baseQuery.sortById(), }; return query.findAll(); } - @override Future> getIdsBySelection(BackupSelection backup) => db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll(); - @override Future> getAllBySelection(BackupSelection backup) => db.backupAlbums.filter().selectionEqualTo(backup).findAll(); - @override - Future deleteAll(List ids) => - txn(() => db.backupAlbums.deleteAll(ids)); + Future deleteAll(List ids) => txn(() => db.backupAlbums.deleteAll(ids)); - @override - Future updateAll(List backupAlbums) => - txn(() => db.backupAlbums.putAll(backupAlbums)); + Future updateAll(List backupAlbums) => txn(() => db.backupAlbums.putAll(backupAlbums)); } diff --git a/mobile/lib/repositories/biometric.repository.dart b/mobile/lib/repositories/biometric.repository.dart index 12d45f8de7..b185501d19 100644 --- a/mobile/lib/repositories/biometric.repository.dart +++ b/mobile/lib/repositories/biometric.repository.dart @@ -3,19 +3,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/auth/biometric_status.model.dart'; import 'package:local_auth/local_auth.dart'; -final biometricRepositoryProvider = - Provider((ref) => BiometricRepository(LocalAuthentication())); +final biometricRepositoryProvider = Provider((ref) => BiometricRepository(LocalAuthentication())); class BiometricRepository { final LocalAuthentication _localAuth; - BiometricRepository(this._localAuth); + const BiometricRepository(this._localAuth); Future getStatus() async { - final bool canAuthenticateWithBiometrics = - await _localAuth.canCheckBiometrics; - final bool canAuthenticate = - canAuthenticateWithBiometrics || await _localAuth.isDeviceSupported(); + final bool canAuthenticateWithBiometrics = await _localAuth.canCheckBiometrics; + final bool canAuthenticate = canAuthenticateWithBiometrics || await _localAuth.isDeviceSupported(); final availableBiometric = await _localAuth.getAvailableBiometrics(); return BiometricStatus( diff --git a/mobile/lib/repositories/database.repository.dart b/mobile/lib/repositories/database.repository.dart index 3eb74621fa..71c15e1c40 100644 --- a/mobile/lib/repositories/database.repository.dart +++ b/mobile/lib/repositories/database.repository.dart @@ -7,16 +7,14 @@ const Symbol _zoneTxn = #zoneTxn; abstract class DatabaseRepository implements IDatabaseRepository { final Isar db; - DatabaseRepository(this.db); + const DatabaseRepository(this.db); bool get inTxn => Zone.current[_zoneTxn] != null; - Future txn(Future Function() callback) => - inTxn ? callback() : transaction(callback); + Future txn(Future Function() callback) => inTxn ? callback() : transaction(callback); @override - Future transaction(Future Function() callback) => - db.writeTxn(callback); + Future transaction(Future Function() callback) => db.writeTxn(callback); } extension Asd on QueryBuilder { diff --git a/mobile/lib/repositories/download.repository.dart b/mobile/lib/repositories/download.repository.dart index 72f7e065ca..5908e31786 100644 --- a/mobile/lib/repositories/download.repository.dart +++ b/mobile/lib/repositories/download.repository.dart @@ -1,10 +1,28 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:background_downloader/background_downloader.dart'; +import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/utils/download.dart'; +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; final downloadRepositoryProvider = Provider((ref) => DownloadRepository()); class DownloadRepository { + static final _downloader = FileDownloader(); + static final _dummyTask = DownloadTask( + taskId: 'dummy', + url: '', + filename: 'dummy', + group: '', + updates: Updates.statusAndProgress, + ); + static final _dummyMetadata = {'part': LivePhotosPart.image, 'id': ''}; + void Function(TaskStatusUpdate)? onImageDownloadStatus; void Function(TaskStatusUpdate)? onVideoDownloadStatus; @@ -14,45 +32,105 @@ class DownloadRepository { void Function(TaskProgressUpdate)? onTaskProgress; DownloadRepository() { - FileDownloader().registerCallbacks( - group: downloadGroupImage, + _downloader.registerCallbacks( + group: kDownloadGroupImage, taskStatusCallback: (update) => onImageDownloadStatus?.call(update), taskProgressCallback: (update) => onTaskProgress?.call(update), ); - FileDownloader().registerCallbacks( - group: downloadGroupVideo, + _downloader.registerCallbacks( + group: kDownloadGroupVideo, taskStatusCallback: (update) => onVideoDownloadStatus?.call(update), taskProgressCallback: (update) => onTaskProgress?.call(update), ); - FileDownloader().registerCallbacks( - group: downloadGroupLivePhoto, + _downloader.registerCallbacks( + group: kDownloadGroupLivePhoto, taskStatusCallback: (update) => onLivePhotoDownloadStatus?.call(update), taskProgressCallback: (update) => onTaskProgress?.call(update), ); } Future> downloadAll(List tasks) { - return FileDownloader().enqueueAll(tasks); + return _downloader.enqueueAll(tasks); } Future deleteAllTrackingRecords() { - return FileDownloader().database.deleteAllRecords(); + return _downloader.database.deleteAllRecords(); } Future cancel(String id) { - return FileDownloader().cancelTaskWithId(id); + return _downloader.cancelTaskWithId(id); } Future> getLiveVideoTasks() { - return FileDownloader().database.allRecordsWithStatus( - TaskStatus.complete, - group: downloadGroupLivePhoto, - ); + return _downloader.database.allRecordsWithStatus( + TaskStatus.complete, + group: kDownloadGroupLivePhoto, + ); } Future deleteRecordsWithIds(List ids) { - return FileDownloader().database.deleteRecordsWithIds(ids); + return _downloader.database.deleteRecordsWithIds(ids); + } + + Future> downloadAllAssets(List assets) async { + if (assets.isEmpty) { + return Future.value(const []); + } + + final length = Platform.isAndroid ? assets.length : assets.length * 2; + final tasks = List.filled(length, _dummyTask); + int taskIndex = 0; + final headers = ApiService.getRequestHeaders(); + for (final asset in assets) { + if (!asset.isRemoteOnly) { + continue; + } + + final id = asset.id; + final livePhotoVideoId = asset.livePhotoVideoId; + final isVideo = asset.isVideo; + final url = getOriginalUrlForRemoteId(id); + + if (Platform.isAndroid || livePhotoVideoId == null || isVideo) { + tasks[taskIndex++] = DownloadTask( + taskId: id, + url: url, + headers: headers, + filename: asset.name, + updates: Updates.statusAndProgress, + group: isVideo ? kDownloadGroupVideo : kDownloadGroupImage, + ); + continue; + } + + _dummyMetadata['part'] = LivePhotosPart.image; + _dummyMetadata['id'] = id; + tasks[taskIndex++] = DownloadTask( + taskId: id, + url: url, + headers: headers, + filename: asset.name, + updates: Updates.statusAndProgress, + group: kDownloadGroupLivePhoto, + metaData: json.encode(_dummyMetadata), + ); + + _dummyMetadata['part'] = LivePhotosPart.video; + tasks[taskIndex++] = DownloadTask( + taskId: livePhotoVideoId, + url: url, + headers: headers, + filename: asset.name.toUpperCase().replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'), + updates: Updates.statusAndProgress, + group: kDownloadGroupLivePhoto, + metaData: json.encode(_dummyMetadata), + ); + } + if (taskIndex == 0) { + return Future.value(const []); + } + return _downloader.enqueueAll(tasks.slice(0, taskIndex)); } } diff --git a/mobile/lib/repositories/drift_album_api_repository.dart b/mobile/lib/repositories/drift_album_api_repository.dart new file mode 100644 index 0000000000..f56cc9fbed --- /dev/null +++ b/mobile/lib/repositories/drift_album_api_repository.dart @@ -0,0 +1,142 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/api.repository.dart'; +// ignore: import_rule_openapi +import 'package:openapi/api.dart'; + +final driftAlbumApiRepositoryProvider = Provider( + (ref) => DriftAlbumApiRepository(ref.watch(apiServiceProvider).albumsApi), +); + +class DriftAlbumApiRepository extends ApiRepository { + final AlbumsApi _api; + + DriftAlbumApiRepository(this._api); + + 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 responseDto.toRemoteAlbum(); + } + + 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<({List added, List failed})> addAssets( + String albumId, + Iterable assetIds, + ) async { + final response = await checkNull( + _api.addAssetsToAlbum( + albumId, + BulkIdsDto(ids: assetIds.toList()), + ), + ); + final List added = [], failed = []; + for (final dto in response) { + if (dto.success) { + added.add(dto.id); + } else { + failed.add(dto.id); + } + } + + return (added: added, failed: failed); + } + + Future updateAlbum( + String albumId, { + String? name, + String? description, + String? thumbnailAssetId, + bool? isActivityEnabled, + AlbumAssetOrder? order, + }) async { + AssetOrder? apiOrder; + if (order != null) { + apiOrder = order == AlbumAssetOrder.asc ? AssetOrder.asc : AssetOrder.desc; + } + + final responseDto = await checkNull( + _api.updateAlbumInfo( + albumId, + UpdateAlbumDto( + albumName: name, + description: description, + albumThumbnailAssetId: thumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: apiOrder, + ), + ), + ); + + return responseDto.toRemoteAlbum(); + } + + Future deleteAlbum(String albumId) { + return _api.deleteAlbum(albumId); + } + + 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 response.toRemoteAlbum(); + } +} + +extension on AlbumResponseDto { + RemoteAlbum toRemoteAlbum() { + return RemoteAlbum( + id: id, + name: albumName, + ownerId: owner.id, + description: description, + createdAt: createdAt, + updatedAt: updatedAt, + thumbnailAssetId: albumThumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, + assetCount: assetCount, + ownerName: owner.name, + ); + } +} diff --git a/mobile/lib/repositories/etag.repository.dart b/mobile/lib/repositories/etag.repository.dart index e8e0624a89..768d95b95c 100644 --- a/mobile/lib/repositories/etag.repository.dart +++ b/mobile/lib/repositories/etag.repository.dart @@ -1,33 +1,24 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/interfaces/etag.interface.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))); +final etagRepositoryProvider = Provider((ref) => ETagRepository(ref.watch(dbProvider))); -class ETagRepository extends DatabaseRepository implements IETagRepository { - ETagRepository(super.db); +class ETagRepository extends DatabaseRepository { + const ETagRepository(super.db); - @override Future> getAllIds() => db.eTags.where().idProperty().findAll(); - @override Future get(String id) => db.eTags.getById(id); - @override Future upsertAll(List etags) => txn(() => db.eTags.putAll(etags)); - @override - Future deleteByIds(List ids) => - txn(() => db.eTags.deleteAllById(ids)); + Future deleteByIds(List ids) => txn(() => db.eTags.deleteAllById(ids)); - @override Future getById(String id) => db.eTags.getById(id); - @override 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 15f7a51e15..86f99e07dd 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -3,14 +3,13 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/interfaces/file_media.interface.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:photo_manager/photo_manager.dart' hide AssetType; -final fileMediaRepositoryProvider = Provider((ref) => FileMediaRepository()); +final fileMediaRepositoryProvider = Provider((ref) => const FileMediaRepository()); -class FileMediaRepository implements IFileMediaRepository { - @override +class FileMediaRepository { + const FileMediaRepository(); Future saveImage( Uint8List data, { required String title, @@ -25,7 +24,6 @@ class FileMediaRepository implements IFileMediaRepository { return AssetMediaRepository.toAsset(entity); } - @override Future saveImageWithFile( String filePath, { String? title, @@ -39,7 +37,6 @@ class FileMediaRepository implements IFileMediaRepository { return AssetMediaRepository.toAsset(entity); } - @override Future saveLivePhoto({ required File image, required File video, @@ -53,7 +50,6 @@ class FileMediaRepository implements IFileMediaRepository { return AssetMediaRepository.toAsset(entity); } - @override Future saveVideo( File file, { required String title, @@ -67,14 +63,9 @@ class FileMediaRepository implements IFileMediaRepository { return AssetMediaRepository.toAsset(entity); } - @override Future clearFileCache() => PhotoManager.clearFileCache(); - @override - Future enableBackgroundAccess() => - PhotoManager.setIgnorePermissionCheck(true); + Future enableBackgroundAccess() => PhotoManager.setIgnorePermissionCheck(true); - @override - Future requestExtendedPermissions() => - PhotoManager.requestPermissionExtend(); + Future requestExtendedPermissions() => PhotoManager.requestPermissionExtend(); } diff --git a/mobile/lib/repositories/folder_api.repository.dart b/mobile/lib/repositories/folder_api.repository.dart index bd7b035157..9fcb57ae21 100644 --- a/mobile/lib/repositories/folder_api.repository.dart +++ b/mobile/lib/repositories/folder_api.repository.dart @@ -1,6 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/interfaces/folder_api.interface.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:logging/logging.dart'; @@ -12,14 +11,12 @@ final folderApiRepositoryProvider = Provider( ), ); -class FolderApiRepository extends ApiRepository - implements IFolderApiRepository { +class FolderApiRepository extends ApiRepository { final ViewApi _api; final Logger _log = Logger("FolderApiRepository"); FolderApiRepository(this._api); - @override Future> getAllUniquePaths() async { try { final list = await _api.getUniqueOriginalPaths(); @@ -30,7 +27,6 @@ class FolderApiRepository extends ApiRepository } } - @override Future> getAssetsForPath(String? path) async { try { final list = await _api.getAssetsByOriginalPath(path ?? '/'); diff --git a/mobile/lib/repositories/gcast.repository.dart b/mobile/lib/repositories/gcast.repository.dart index 11c149ab37..f896de2ab0 100644 --- a/mobile/lib/repositories/gcast.repository.dart +++ b/mobile/lib/repositories/gcast.repository.dart @@ -69,7 +69,6 @@ class GCastRepository { } Future> listDestinations() async { - return await CastDiscoveryService() - .search(timeout: const Duration(seconds: 3)); + return await CastDiscoveryService().search(timeout: const Duration(seconds: 3)); } } diff --git a/mobile/lib/repositories/local_files_manager.repository.dart b/mobile/lib/repositories/local_files_manager.repository.dart index c2e234d14d..519d79b49b 100644 --- a/mobile/lib/repositories/local_files_manager.repository.dart +++ b/mobile/lib/repositories/local_files_manager.repository.dart @@ -1,25 +1,24 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/interfaces/local_files_manager.interface.dart'; -import 'package:immich_mobile/utils/local_files_manager.dart'; +import 'package:immich_mobile/services/local_files_manager.service.dart'; -final localFilesManagerRepositoryProvider = - Provider((ref) => const LocalFilesManagerRepository()); +final localFilesManagerRepositoryProvider = Provider( + (ref) => LocalFilesManagerRepository(ref.watch(localFileManagerServiceProvider)), +); -class LocalFilesManagerRepository implements ILocalFilesManager { - const LocalFilesManagerRepository(); +class LocalFilesManagerRepository { + const LocalFilesManagerRepository(this._service); + + final LocalFilesManagerService _service; - @override Future moveToTrash(List mediaUrls) async { - return await LocalFilesManager.moveToTrash(mediaUrls); + return await _service.moveToTrash(mediaUrls); } - @override Future restoreFromTrash(String fileName, int type) async { - return await LocalFilesManager.restoreFromTrash(fileName, type); + return await _service.restoreFromTrash(fileName, type); } - @override Future requestManageMediaPermission() async { - return await LocalFilesManager.requestManageMediaPermission(); + return await _service.requestManageMediaPermission(); } } diff --git a/mobile/lib/repositories/network.repository.dart b/mobile/lib/repositories/network.repository.dart index e80b406b10..2f859e9452 100644 --- a/mobile/lib/repositories/network.repository.dart +++ b/mobile/lib/repositories/network.repository.dart @@ -12,7 +12,7 @@ final networkRepositoryProvider = Provider((_) { class NetworkRepository { final NetworkInfo _networkInfo; - NetworkRepository(this._networkInfo); + const NetworkRepository(this._networkInfo); Future getWifiName() { if (Platform.isAndroid) { diff --git a/mobile/lib/repositories/partner.repository.dart b/mobile/lib/repositories/partner.repository.dart index 8a53ca7c8d..23b6fadebb 100644 --- a/mobile/lib/repositories/partner.repository.dart +++ b/mobile/lib/repositories/partner.repository.dart @@ -1,7 +1,6 @@ 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/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'; @@ -11,24 +10,14 @@ final partnerRepositoryProvider = Provider( ); class PartnerRepository extends DatabaseRepository { - PartnerRepository(super.db); + const PartnerRepository(super.db); Future> getSharedBy() async { - return (await db.users - .filter() - .isPartnerSharedByEqualTo(true) - .sortById() - .findAll()) - .map((u) => u.toDto()) - .toList(); + 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()) + return (await db.users.filter().isPartnerSharedWithEqualTo(true).sortById().findAll()) .map((u) => u.toDto()) .toList(); } @@ -39,11 +28,7 @@ class PartnerRepository extends DatabaseRepository { } Stream> watchSharedWith() { - return (db.users - .filter() - .isPartnerSharedWithEqualTo(true) - .sortById() - .watch()) + 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 836a708e3a..00de1ea2f1 100644 --- a/mobile/lib/repositories/partner_api.repository.dart +++ b/mobile/lib/repositories/partner_api.repository.dart @@ -24,9 +24,7 @@ class PartnerApiRepository extends ApiRepository { Future> getAll(Direction direction) async { final response = await checkNull( _api.getPartners( - direction == Direction.sharedByMe - ? PartnerDirection.by - : PartnerDirection.with_, + direction == Direction.sharedByMe ? PartnerDirection.by : PartnerDirection.with_, ), ); return response.map(UserConverter.fromPartnerDto).toList(); diff --git a/mobile/lib/repositories/permission.repository.dart b/mobile/lib/repositories/permission.repository.dart index f825c36075..74230a3652 100644 --- a/mobile/lib/repositories/permission.repository.dart +++ b/mobile/lib/repositories/permission.repository.dart @@ -2,11 +2,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:permission_handler/permission_handler.dart'; final permissionRepositoryProvider = Provider((_) { - return PermissionRepository(); + return const PermissionRepository(); }); class PermissionRepository implements IPermissionRepository { - PermissionRepository(); + const PermissionRepository(); @override Future hasLocationWhenInUsePermission() { diff --git a/mobile/lib/repositories/person_api.repository.dart b/mobile/lib/repositories/person_api.repository.dart index a2a6e2489b..26f11dd51d 100644 --- a/mobile/lib/repositories/person_api.repository.dart +++ b/mobile/lib/repositories/person_api.repository.dart @@ -13,19 +13,19 @@ class PersonApiRepository extends ApiRepository { PersonApiRepository(this._api); - Future> getAll() async { + Future> getAll() async { final dto = await checkNull(_api.getAllPeople()); return dto.people.map(_toPerson).toList(); } - Future update(String id, {String? name}) async { + Future update(String id, {String? name}) async { final dto = await checkNull( _api.updatePerson(id, PersonUpdateDto(name: name)), ); return _toPerson(dto); } - static Person _toPerson(PersonResponseDto dto) => Person( + static PersonDto _toPerson(PersonResponseDto dto) => PersonDto( birthDate: dto.birthDate, id: dto.id, isHidden: dto.isHidden, diff --git a/mobile/lib/repositories/secure_storage.repository.dart b/mobile/lib/repositories/secure_storage.repository.dart index 08d47b0525..9ae643a587 100644 --- a/mobile/lib/repositories/secure_storage.repository.dart +++ b/mobile/lib/repositories/secure_storage.repository.dart @@ -1,13 +1,12 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -final secureStorageRepositoryProvider = - Provider((ref) => SecureStorageRepository(const FlutterSecureStorage())); +final secureStorageRepositoryProvider = Provider((ref) => const SecureStorageRepository(FlutterSecureStorage())); class SecureStorageRepository { final FlutterSecureStorage _secureStorage; - SecureStorageRepository(this._secureStorage); + const SecureStorageRepository(this._secureStorage); Future read(String key) { return _secureStorage.read(key: key); diff --git a/mobile/lib/repositories/timeline.repository.dart b/mobile/lib/repositories/timeline.repository.dart index 0120ea85ad..1579300bc6 100644 --- a/mobile/lib/repositories/timeline.repository.dart +++ b/mobile/lib/repositories/timeline.repository.dart @@ -9,30 +9,17 @@ 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))); +final timelineRepositoryProvider = Provider((ref) => TimelineRepository(ref.watch(dbProvider))); class TimelineRepository extends DatabaseRepository { - TimelineRepository(super.db); + const TimelineRepository(super.db); Future> getTimelineUserIds(String id) { - return db.users - .filter() - .inTimelineEqualTo(true) - .or() - .idEqualTo(id) - .idProperty() - .findAll(); + 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(); + return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().watch(); } Stream watchArchiveTimeline(String userId) { @@ -65,11 +52,7 @@ class TimelineRepository extends DatabaseRepository { Album album, GroupAssetsBy groupAssetByOption, ) { - final query = album.assets - .filter() - .isTrashedEqualTo(false) - .not() - .visibilityEqualTo(AssetVisibilityEnum.locked); + final query = album.assets.filter().isTrashedEqualTo(false).not().visibilityEqualTo(AssetVisibilityEnum.locked); final withSortedOption = switch (album.sortOrder) { SortOrder.asc => query.sortByFileCreatedAt(), @@ -80,11 +63,7 @@ class TimelineRepository extends DatabaseRepository { } Stream watchTrashTimeline(String userId) { - final query = db.assets - .filter() - .ownerIdEqualTo(fastHash(userId)) - .isTrashedEqualTo(true) - .sortByFileCreatedAtDesc(); + final query = db.assets.filter().ownerIdEqualTo(fastHash(userId)).isTrashedEqualTo(true).sortByFileCreatedAtDesc(); return _watchRenderList(query, GroupAssetsBy.none); } diff --git a/mobile/lib/repositories/upload.repository.dart b/mobile/lib/repositories/upload.repository.dart index 6445d144f6..1510eb208f 100644 --- a/mobile/lib/repositories/upload.repository.dart +++ b/mobile/lib/repositories/upload.repository.dart @@ -1,42 +1,88 @@ import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/interfaces/upload.interface.dart'; -import 'package:immich_mobile/utils/upload.dart'; +import 'package:immich_mobile/constants/constants.dart'; final uploadRepositoryProvider = Provider((ref) => UploadRepository()); -class UploadRepository implements IUploadRepository { - @override +class UploadRepository { void Function(TaskStatusUpdate)? onUploadStatus; - - @override void Function(TaskProgressUpdate)? onTaskProgress; UploadRepository() { FileDownloader().registerCallbacks( - group: uploadGroup, + group: kBackupGroup, + taskStatusCallback: (update) => onUploadStatus?.call(update), + taskProgressCallback: (update) => onTaskProgress?.call(update), + ); + FileDownloader().registerCallbacks( + group: kBackupLivePhotoGroup, + taskStatusCallback: (update) => onUploadStatus?.call(update), + taskProgressCallback: (update) => onTaskProgress?.call(update), + ); + FileDownloader().registerCallbacks( + group: kManualUploadGroup, taskStatusCallback: (update) => onUploadStatus?.call(update), taskProgressCallback: (update) => onTaskProgress?.call(update), ); } - @override - Future upload(UploadTask task) { - return FileDownloader().enqueue(task); + void enqueueBackgroundAll(List tasks) { + FileDownloader().enqueueAll(tasks); } - @override - Future deleteAllTrackingRecords() { - return FileDownloader().database.deleteAllRecords(); + Future deleteDatabaseRecords(String group) { + return FileDownloader().database.deleteAllRecords(group: group); } - @override - Future cancel(String id) { - return FileDownloader().cancelTaskWithId(id); + Future cancelAll(String group) { + return FileDownloader().cancelAll(group: group); } - @override - Future deleteRecordsWithIds(List ids) { - return FileDownloader().database.deleteRecordsWithIds(ids); + Future reset(String group) { + return FileDownloader().reset(group: group); + } + + /// Get a list of tasks that are ENQUEUED or RUNNING + Future> getActiveTasks(String group) { + return FileDownloader().allTasks(group: group); + } + + Future start() { + return FileDownloader().start(); + } + + Future getUploadInfo() async { + final [enqueuedTasks, runningTasks, canceledTasks, waitingTasks, pausedTasks] = await Future.wait([ + FileDownloader().database.allRecordsWithStatus( + TaskStatus.enqueued, + group: kBackupGroup, + ), + FileDownloader().database.allRecordsWithStatus( + TaskStatus.running, + group: kBackupGroup, + ), + FileDownloader().database.allRecordsWithStatus( + TaskStatus.canceled, + group: kBackupGroup, + ), + FileDownloader().database.allRecordsWithStatus( + TaskStatus.waitingToRetry, + group: kBackupGroup, + ), + FileDownloader().database.allRecordsWithStatus( + TaskStatus.paused, + group: kBackupGroup, + ), + ]); + + debugPrint(""" + Upload Info: + Enqueued: ${enqueuedTasks.length} + Running: ${runningTasks.length} + Canceled: ${canceledTasks.length} + Waiting: ${waitingTasks.length} + Paused: ${pausedTasks.length} + """); } } diff --git a/mobile/lib/repositories/widget.repository.dart b/mobile/lib/repositories/widget.repository.dart index a813bc56d6..09532f4b78 100644 --- a/mobile/lib/repositories/widget.repository.dart +++ b/mobile/lib/repositories/widget.repository.dart @@ -1,23 +1,22 @@ import 'package:home_widget/home_widget.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/interfaces/widget.interface.dart'; -final widgetRepositoryProvider = Provider((_) => WidgetRepository()); +final widgetRepositoryProvider = Provider((_) => const WidgetRepository()); -class WidgetRepository implements IWidgetRepository { - WidgetRepository(); +class WidgetRepository { + const WidgetRepository(); - @override Future saveData(String key, String value) async { await HomeWidget.saveWidgetData(key, value); } - @override - Future refresh(String name) async { - await HomeWidget.updateWidget(name: name, iOSName: name); + Future refresh(String iosName, String androidName) async { + await HomeWidget.updateWidget( + iOSName: iosName, + qualifiedAndroidName: androidName, + ); } - @override Future setAppGroupId(String appGroupId) async { await HomeWidget.setAppGroupId(appGroupId); } diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index 44662c0b8b..7e5d73cae8 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -25,19 +25,40 @@ 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, + ); } _handleLockedViewState(Route route, Route? previousRoute) { final isInLockedView = ref.read(inLockedViewProvider); final isFromLockedViewToDetailView = - route.settings.name == GalleryViewerRoute.name && - previousRoute?.settings.name == LockedRoute.name; + route.settings.name == GalleryViewerRoute.name && previousRoute?.settings.name == LockedRoute.name; - final isFromDetailViewToInfoPanelView = route.settings.name == null && - previousRoute?.settings.name == GalleryViewerRoute.name && - isInLockedView; + final isFromDetailViewToInfoPanelView = + route.settings.name == null && previousRoute?.settings.name == GalleryViewerRoute.name && isInLockedView; - if (route.settings.name == LockedRoute.name || + 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 = + route.settings.name == AssetViewerRoute.name && previousRoute?.settings.name == DriftLockedFolderRoute.name; + + final isFromDetailViewToInfoPanelView = + route.settings.name == null && previousRoute?.settings.name == AssetViewerRoute.name && isInLockedView; + + if (route.settings.name == DriftLockedFolderRoute.name || isFromLockedViewToDetailView || isFromDetailViewToInfoPanelView) { Future( diff --git a/mobile/lib/routing/backup_permission_guard.dart b/mobile/lib/routing/backup_permission_guard.dart index 57a0c7a927..245a4b27af 100644 --- a/mobile/lib/routing/backup_permission_guard.dart +++ b/mobile/lib/routing/backup_permission_guard.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/routing/router.dart'; class BackupPermissionGuard extends AutoRouteGuard { final GalleryPermissionNotifier _permission; - BackupPermissionGuard(this._permission); + const BackupPermissionGuard(this._permission); @override void onNavigation(NavigationResolver resolver, StackRouter router) async { diff --git a/mobile/lib/routing/custom_transition_builders.dart b/mobile/lib/routing/custom_transition_builders.dart index 610edd8185..d8412eb7cf 100644 --- a/mobile/lib/routing/custom_transition_builders.dart +++ b/mobile/lib/routing/custom_transition_builders.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; class CustomTransitionsBuilders { const CustomTransitionsBuilders._(); - static const ZoomPageTransitionsBuilder zoomPageTransitionsBuilder = - ZoomPageTransitionsBuilder(); + static const ZoomPageTransitionsBuilder zoomPageTransitionsBuilder = ZoomPageTransitionsBuilder(); static const RouteTransitionsBuilder zoomedPage = _zoomedPage; @@ -19,8 +18,7 @@ class CustomTransitionsBuilders { PageRouteBuilder( allowSnapshotting: true, fullscreenDialog: false, - pageBuilder: (context, animation, secondaryAnimation) => - const SizedBox.shrink(), + pageBuilder: (context, animation, secondaryAnimation) => const SizedBox.shrink(), ), context, animation, diff --git a/mobile/lib/routing/duplicate_guard.dart b/mobile/lib/routing/duplicate_guard.dart index 217ff1cbf4..efc649bc41 100644 --- a/mobile/lib/routing/duplicate_guard.dart +++ b/mobile/lib/routing/duplicate_guard.dart @@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart'; /// Guards against duplicate navigation to this route class DuplicateGuard extends AutoRouteGuard { - DuplicateGuard(); + const DuplicateGuard(); @override void onNavigation(NavigationResolver resolver, StackRouter router) async { // Duplicate navigation diff --git a/mobile/lib/routing/gallery_guard.dart b/mobile/lib/routing/gallery_guard.dart new file mode 100644 index 0000000000..eace8257b6 --- /dev/null +++ b/mobile/lib/routing/gallery_guard.dart @@ -0,0 +1,30 @@ +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; + + 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 3e1563dd25..a72558508c 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -1,8 +1,13 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/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/log.model.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/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'; @@ -17,13 +22,17 @@ 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/drift_backup_album_selection.page.dart'; +import 'package:immich_mobile/pages/backup/drift_backup.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_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'; @@ -31,6 +40,7 @@ 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'; @@ -41,6 +51,7 @@ 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'; @@ -62,17 +73,38 @@ import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart'; import 'package:immich_mobile/pages/search/person_result.page.dart'; import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; +import 'package:immich_mobile/pages/settings/beta_sync_settings.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_archive.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_library.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_local_album.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_locked_folder.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_memory.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_partner_detail.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_place.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; +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_video.page.dart'; +import 'package:immich_mobile/presentation/pages/local_timeline.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'; @@ -97,6 +129,7 @@ class AppRouter extends RootStackRouter { late final DuplicateGuard _duplicateGuard; late final BackupPermissionGuard _backupPermissionGuard; late final LockedGuard _lockedGuard; + late final GalleryGuard _galleryGuard; AppRouter( ApiService apiService, @@ -105,10 +138,10 @@ class AppRouter extends RootStackRouter { LocalAuthService localAuthService, ) { _authGuard = AuthGuard(apiService); - _duplicateGuard = DuplicateGuard(); - _lockedGuard = - LockedGuard(apiService, secureStorageService, localAuthService); + _duplicateGuard = const DuplicateGuard(); + _lockedGuard = LockedGuard(apiService, secureStorageService, localAuthService); _backupPermissionGuard = BackupPermissionGuard(galleryPermissionNotifier); + _galleryGuard = const GalleryGuard(); } @override @@ -153,8 +186,32 @@ class AppRouter extends RootStackRouter { transitionsBuilder: TransitionsBuilders.fadeIn, ), CustomRoute( - page: GalleryViewerRoute.page, + page: TabShellRoute.page, guards: [_authGuard, _duplicateGuard], + children: [ + AutoRoute( + page: MainTimelineRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftSearchRoute.page, + guards: [_authGuard, _duplicateGuard], + maintainState: false, + ), + AutoRoute( + page: DriftLibraryRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftAlbumsRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + ], + transitionsBuilder: TransitionsBuilders.fadeIn, + ), + CustomRoute( + page: GalleryViewerRoute.page, + guards: [_authGuard, _galleryGuard], transitionsBuilder: CustomTransitionsBuilders.zoomedPage, ), AutoRoute( @@ -332,6 +389,14 @@ class AppRouter extends RootStackRouter { page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: DriftBackupRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftBackupAlbumSelectionRoute.page, + guards: [_authGuard, _duplicateGuard], + ), AutoRoute( page: LocalTimelineRoute.page, guards: [_authGuard, _duplicateGuard], @@ -340,5 +405,101 @@ class AppRouter extends RootStackRouter { page: MainTimelineRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: RemoteAlbumRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: AssetViewerRoute.page, + guards: [_authGuard, _duplicateGuard], + type: RouteType.custom( + customRouteBuilder: (context, child, page) => PageRouteBuilder( + fullscreenDialog: page.fullscreenDialog, + settings: page, + pageBuilder: (_, __, ___) => child, + opaque: false, + ), + ), + ), + AutoRoute( + page: DriftMemoryRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftFavoriteRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftTrashRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftArchiveRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftLockedFolderRoute.page, + guards: [_authGuard, _lockedGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftVideoRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftLibraryRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftAssetSelectionTimelineRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftPartnerDetailRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftRecentlyTakenRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftLocalAlbumsRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftCreateAlbumRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + 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: BetaSyncSettingsRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + // required to handle all deeplinks in deep_link.service.dart + // auto_route_library#1722 + RedirectRoute(path: '*', redirectTo: '/'), ]; } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index efc9e71a23..6b7e479cff 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -13,8 +13,7 @@ part of 'router.dart'; /// generated route for /// [ActivitiesPage] class ActivitiesRoute extends PageRouteInfo { - const ActivitiesRoute({List? children}) - : super(ActivitiesRoute.name, initialChildren: children); + const ActivitiesRoute({List? children}) : super(ActivitiesRoute.name, initialChildren: children); static const String name = 'ActivitiesRoute'; @@ -28,8 +27,7 @@ class ActivitiesRoute extends PageRouteInfo { /// generated route for /// [AlbumAdditionalSharedUserSelectionPage] -class AlbumAdditionalSharedUserSelectionRoute - extends PageRouteInfo { +class AlbumAdditionalSharedUserSelectionRoute extends PageRouteInfo { AlbumAdditionalSharedUserSelectionRoute({ Key? key, required Album album, @@ -75,8 +73,7 @@ class AlbumAdditionalSharedUserSelectionRouteArgs { /// generated route for /// [AlbumAssetSelectionPage] -class AlbumAssetSelectionRoute - extends PageRouteInfo { +class AlbumAssetSelectionRoute extends PageRouteInfo { AlbumAssetSelectionRoute({ Key? key, required Set existingAssets, @@ -129,8 +126,7 @@ class AlbumAssetSelectionRouteArgs { /// generated route for /// [AlbumOptionsPage] class AlbumOptionsRoute extends PageRouteInfo { - const AlbumOptionsRoute({List? children}) - : super(AlbumOptionsRoute.name, initialChildren: children); + const AlbumOptionsRoute({List? children}) : super(AlbumOptionsRoute.name, initialChildren: children); static const String name = 'AlbumOptionsRoute'; @@ -181,8 +177,7 @@ class AlbumPreviewRouteArgs { /// generated route for /// [AlbumSharedUserSelectionPage] -class AlbumSharedUserSelectionRoute - extends PageRouteInfo { +class AlbumSharedUserSelectionRoute extends PageRouteInfo { AlbumSharedUserSelectionRoute({ Key? key, required Set assets, @@ -257,8 +252,7 @@ class AlbumViewerRouteArgs { /// generated route for /// [AlbumsPage] class AlbumsRoute extends PageRouteInfo { - const AlbumsRoute({List? children}) - : super(AlbumsRoute.name, initialChildren: children); + const AlbumsRoute({List? children}) : super(AlbumsRoute.name, initialChildren: children); static const String name = 'AlbumsRoute'; @@ -289,8 +283,7 @@ class AllMotionPhotosRoute extends PageRouteInfo { /// generated route for /// [AllPeoplePage] class AllPeopleRoute extends PageRouteInfo { - const AllPeopleRoute({List? children}) - : super(AllPeopleRoute.name, initialChildren: children); + const AllPeopleRoute({List? children}) : super(AllPeopleRoute.name, initialChildren: children); static const String name = 'AllPeopleRoute'; @@ -305,8 +298,7 @@ class AllPeopleRoute extends PageRouteInfo { /// generated route for /// [AllPlacesPage] class AllPlacesRoute extends PageRouteInfo { - const AllPlacesRoute({List? children}) - : super(AllPlacesRoute.name, initialChildren: children); + const AllPlacesRoute({List? children}) : super(AllPlacesRoute.name, initialChildren: children); static const String name = 'AllPlacesRoute'; @@ -321,8 +313,7 @@ class AllPlacesRoute extends PageRouteInfo { /// generated route for /// [AllVideosPage] class AllVideosRoute extends PageRouteInfo { - const AllVideosRoute({List? children}) - : super(AllVideosRoute.name, initialChildren: children); + const AllVideosRoute({List? children}) : super(AllVideosRoute.name, initialChildren: children); static const String name = 'AllVideosRoute'; @@ -374,8 +365,7 @@ class AppLogDetailRouteArgs { /// generated route for /// [AppLogPage] class AppLogRoute extends PageRouteInfo { - const AppLogRoute({List? children}) - : super(AppLogRoute.name, initialChildren: children); + const AppLogRoute({List? children}) : super(AppLogRoute.name, initialChildren: children); static const String name = 'AppLogRoute'; @@ -390,8 +380,7 @@ class AppLogRoute extends PageRouteInfo { /// generated route for /// [ArchivePage] class ArchiveRoute extends PageRouteInfo { - const ArchiveRoute({List? children}) - : super(ArchiveRoute.name, initialChildren: children); + const ArchiveRoute({List? children}) : super(ArchiveRoute.name, initialChildren: children); static const String name = 'ArchiveRoute'; @@ -403,6 +392,64 @@ class ArchiveRoute extends PageRouteInfo { ); } +/// generated route for +/// [AssetViewerPage] +class AssetViewerRoute extends PageRouteInfo { + AssetViewerRoute({ + Key? key, + required int initialIndex, + required TimelineService timelineService, + int? heroOffset, + List? children, + }) : super( + AssetViewerRoute.name, + args: AssetViewerRouteArgs( + key: key, + initialIndex: initialIndex, + timelineService: timelineService, + heroOffset: heroOffset, + ), + initialChildren: children, + ); + + static const String name = 'AssetViewerRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AssetViewerPage( + key: args.key, + initialIndex: args.initialIndex, + timelineService: args.timelineService, + heroOffset: args.heroOffset, + ); + }, + ); +} + +class AssetViewerRouteArgs { + const AssetViewerRouteArgs({ + this.key, + required this.initialIndex, + required this.timelineService, + this.heroOffset, + }); + + final Key? key; + + final int initialIndex; + + final TimelineService timelineService; + + final int? heroOffset; + + @override + String toString() { + return 'AssetViewerRouteArgs{key: $key, initialIndex: $initialIndex, timelineService: $timelineService, heroOffset: $heroOffset}'; + } +} + /// generated route for /// [BackupAlbumSelectionPage] class BackupAlbumSelectionRoute extends PageRouteInfo { @@ -438,8 +485,7 @@ class BackupControllerRoute extends PageRouteInfo { /// generated route for /// [BackupOptionsPage] class BackupOptionsRoute extends PageRouteInfo { - const BackupOptionsRoute({List? children}) - : super(BackupOptionsRoute.name, initialChildren: children); + const BackupOptionsRoute({List? children}) : super(BackupOptionsRoute.name, initialChildren: children); static const String name = 'BackupOptionsRoute'; @@ -451,6 +497,65 @@ class BackupOptionsRoute extends PageRouteInfo { ); } +/// generated route for +/// [BetaSyncSettingsPage] +class BetaSyncSettingsRoute extends PageRouteInfo { + const BetaSyncSettingsRoute({List? children}) + : super(BetaSyncSettingsRoute.name, initialChildren: children); + + static const String name = 'BetaSyncSettingsRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const BetaSyncSettingsPage(); + }, + ); +} + +/// generated route for +/// [ChangeExperiencePage] +class ChangeExperienceRoute extends PageRouteInfo { + 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}'; + } +} + /// generated route for /// [ChangePasswordPage] class ChangePasswordRoute extends PageRouteInfo { @@ -550,6 +655,514 @@ class CropImageRouteArgs { } } +/// generated route for +/// [DriftAlbumsPage] +class DriftAlbumsRoute extends PageRouteInfo { + const DriftAlbumsRoute({List? children}) : super(DriftAlbumsRoute.name, initialChildren: children); + + static const String name = 'DriftAlbumsRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftAlbumsPage(); + }, + ); +} + +/// generated route for +/// [DriftArchivePage] +class DriftArchiveRoute extends PageRouteInfo { + const DriftArchiveRoute({List? children}) : super(DriftArchiveRoute.name, initialChildren: children); + + static const String name = 'DriftArchiveRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftArchivePage(); + }, + ); +} + +/// generated route for +/// [DriftAssetSelectionTimelinePage] +class DriftAssetSelectionTimelineRoute extends PageRouteInfo { + DriftAssetSelectionTimelineRoute({ + Key? key, + Set lockedSelectionAssets = const {}, + List? children, + }) : super( + DriftAssetSelectionTimelineRoute.name, + args: DriftAssetSelectionTimelineRouteArgs( + key: key, + lockedSelectionAssets: lockedSelectionAssets, + ), + initialChildren: children, + ); + + static const String name = 'DriftAssetSelectionTimelineRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const DriftAssetSelectionTimelineRouteArgs(), + ); + return DriftAssetSelectionTimelinePage( + key: args.key, + lockedSelectionAssets: args.lockedSelectionAssets, + ); + }, + ); +} + +class DriftAssetSelectionTimelineRouteArgs { + const DriftAssetSelectionTimelineRouteArgs({ + this.key, + this.lockedSelectionAssets = const {}, + }); + + final Key? key; + + final Set lockedSelectionAssets; + + @override + String toString() { + return 'DriftAssetSelectionTimelineRouteArgs{key: $key, lockedSelectionAssets: $lockedSelectionAssets}'; + } +} + +/// generated route for +/// [DriftBackupAlbumSelectionPage] +class DriftBackupAlbumSelectionRoute extends PageRouteInfo { + const DriftBackupAlbumSelectionRoute({List? children}) + : super(DriftBackupAlbumSelectionRoute.name, initialChildren: children); + + static const String name = 'DriftBackupAlbumSelectionRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftBackupAlbumSelectionPage(); + }, + ); +} + +/// generated route for +/// [DriftBackupPage] +class DriftBackupRoute extends PageRouteInfo { + const DriftBackupRoute({List? children}) : super(DriftBackupRoute.name, initialChildren: children); + + static const String name = 'DriftBackupRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftBackupPage(); + }, + ); +} + +/// generated route for +/// [DriftCreateAlbumPage] +class DriftCreateAlbumRoute extends PageRouteInfo { + const DriftCreateAlbumRoute({List? children}) + : super(DriftCreateAlbumRoute.name, initialChildren: children); + + static const String name = 'DriftCreateAlbumRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftCreateAlbumPage(); + }, + ); +} + +/// generated route for +/// [DriftFavoritePage] +class DriftFavoriteRoute extends PageRouteInfo { + const DriftFavoriteRoute({List? children}) : super(DriftFavoriteRoute.name, initialChildren: children); + + static const String name = 'DriftFavoriteRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftFavoritePage(); + }, + ); +} + +/// generated route for +/// [DriftLibraryPage] +class DriftLibraryRoute extends PageRouteInfo { + const DriftLibraryRoute({List? children}) : super(DriftLibraryRoute.name, initialChildren: children); + + static const String name = 'DriftLibraryRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftLibraryPage(); + }, + ); +} + +/// generated route for +/// [DriftLocalAlbumsPage] +class DriftLocalAlbumsRoute extends PageRouteInfo { + const DriftLocalAlbumsRoute({List? children}) + : super(DriftLocalAlbumsRoute.name, initialChildren: children); + + static const String name = 'DriftLocalAlbumsRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftLocalAlbumsPage(); + }, + ); +} + +/// generated route for +/// [DriftLockedFolderPage] +class DriftLockedFolderRoute extends PageRouteInfo { + const DriftLockedFolderRoute({List? children}) + : super(DriftLockedFolderRoute.name, initialChildren: children); + + static const String name = 'DriftLockedFolderRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftLockedFolderPage(); + }, + ); +} + +/// generated route for +/// [DriftMemoryPage] +class DriftMemoryRoute extends PageRouteInfo { + DriftMemoryRoute({ + required List memories, + required int memoryIndex, + Key? key, + List? children, + }) : super( + DriftMemoryRoute.name, + args: DriftMemoryRouteArgs( + memories: memories, + memoryIndex: memoryIndex, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'DriftMemoryRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return DriftMemoryPage( + memories: args.memories, + memoryIndex: args.memoryIndex, + key: args.key, + ); + }, + ); +} + +class DriftMemoryRouteArgs { + const DriftMemoryRouteArgs({ + required this.memories, + required this.memoryIndex, + this.key, + }); + + final List memories; + + final int memoryIndex; + + final Key? key; + + @override + String toString() { + return 'DriftMemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; + } +} + +/// generated route for +/// [DriftPartnerDetailPage] +class DriftPartnerDetailRoute extends PageRouteInfo { + DriftPartnerDetailRoute({ + Key? key, + required PartnerUserDto partner, + List? children, + }) : super( + DriftPartnerDetailRoute.name, + args: DriftPartnerDetailRouteArgs(key: key, partner: partner), + initialChildren: children, + ); + + static const String name = 'DriftPartnerDetailRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return DriftPartnerDetailPage(key: args.key, partner: args.partner); + }, + ); +} + +class DriftPartnerDetailRouteArgs { + const DriftPartnerDetailRouteArgs({this.key, required this.partner}); + + final Key? key; + + final PartnerUserDto partner; + + @override + String toString() { + return 'DriftPartnerDetailRouteArgs{key: $key, partner: $partner}'; + } +} + +/// generated route for +/// [DriftPartnerPage] +class DriftPartnerRoute extends PageRouteInfo { + const DriftPartnerRoute({List? children}) : super(DriftPartnerRoute.name, initialChildren: children); + + static const String name = 'DriftPartnerRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftPartnerPage(); + }, + ); +} + +/// generated route for +/// [DriftPlaceDetailPage] +class DriftPlaceDetailRoute extends PageRouteInfo { + DriftPlaceDetailRoute({ + Key? key, + required String place, + List? children, + }) : super( + DriftPlaceDetailRoute.name, + args: DriftPlaceDetailRouteArgs(key: key, place: place), + initialChildren: children, + ); + + static const String name = 'DriftPlaceDetailRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return DriftPlaceDetailPage(key: args.key, place: args.place); + }, + ); +} + +class DriftPlaceDetailRouteArgs { + const DriftPlaceDetailRouteArgs({this.key, required this.place}); + + final Key? key; + + final String place; + + @override + String toString() { + return 'DriftPlaceDetailRouteArgs{key: $key, place: $place}'; + } +} + +/// generated route for +/// [DriftPlacePage] +class DriftPlaceRoute extends PageRouteInfo { + DriftPlaceRoute({ + Key? key, + LatLng? currentLocation, + List? children, + }) : super( + DriftPlaceRoute.name, + args: DriftPlaceRouteArgs(key: key, currentLocation: currentLocation), + initialChildren: children, + ); + + static const String name = 'DriftPlaceRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const DriftPlaceRouteArgs(), + ); + return DriftPlacePage( + key: args.key, + currentLocation: args.currentLocation, + ); + }, + ); +} + +class DriftPlaceRouteArgs { + const DriftPlaceRouteArgs({this.key, this.currentLocation}); + + final Key? key; + + final LatLng? currentLocation; + + @override + String toString() { + return 'DriftPlaceRouteArgs{key: $key, currentLocation: $currentLocation}'; + } +} + +/// generated route for +/// [DriftRecentlyTakenPage] +class DriftRecentlyTakenRoute extends PageRouteInfo { + const DriftRecentlyTakenRoute({List? children}) + : super(DriftRecentlyTakenRoute.name, initialChildren: children); + + static const String name = 'DriftRecentlyTakenRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftRecentlyTakenPage(); + }, + ); +} + +/// generated route for +/// [DriftSearchPage] +class DriftSearchRoute extends PageRouteInfo { + DriftSearchRoute({ + Key? key, + SearchFilter? preFilter, + List? children, + }) : super( + DriftSearchRoute.name, + args: DriftSearchRouteArgs(key: key, preFilter: preFilter), + initialChildren: children, + ); + + static const String name = 'DriftSearchRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const DriftSearchRouteArgs(), + ); + return DriftSearchPage(key: args.key, preFilter: args.preFilter); + }, + ); +} + +class DriftSearchRouteArgs { + const DriftSearchRouteArgs({this.key, this.preFilter}); + + final Key? key; + + final SearchFilter? preFilter; + + @override + String toString() { + return 'DriftSearchRouteArgs{key: $key, preFilter: $preFilter}'; + } +} + +/// generated route for +/// [DriftTrashPage] +class DriftTrashRoute extends PageRouteInfo { + const DriftTrashRoute({List? children}) : super(DriftTrashRoute.name, initialChildren: children); + + static const String name = 'DriftTrashRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftTrashPage(); + }, + ); +} + +/// generated route for +/// [DriftUploadDetailPage] +class DriftUploadDetailRoute extends PageRouteInfo { + const DriftUploadDetailRoute({List? children}) + : super(DriftUploadDetailRoute.name, initialChildren: children); + + static const String name = 'DriftUploadDetailRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftUploadDetailPage(); + }, + ); +} + +/// generated route for +/// [DriftUserSelectionPage] +class DriftUserSelectionRoute extends PageRouteInfo { + DriftUserSelectionRoute({ + Key? key, + required RemoteAlbum album, + List? children, + }) : super( + DriftUserSelectionRoute.name, + args: DriftUserSelectionRouteArgs(key: key, album: album), + initialChildren: children, + ); + + static const String name = 'DriftUserSelectionRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return DriftUserSelectionPage(key: args.key, album: args.album); + }, + ); +} + +class DriftUserSelectionRouteArgs { + const DriftUserSelectionRouteArgs({this.key, required this.album}); + + final Key? key; + + final RemoteAlbum album; + + @override + String toString() { + return 'DriftUserSelectionRouteArgs{key: $key, album: $album}'; + } +} + +/// generated route for +/// [DriftVideoPage] +class DriftVideoRoute extends PageRouteInfo { + const DriftVideoRoute({List? children}) : super(DriftVideoRoute.name, initialChildren: children); + + static const String name = 'DriftVideoRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftVideoPage(); + }, + ); +} + /// generated route for /// [EditImagePage] class EditImageRoute extends PageRouteInfo { @@ -627,8 +1240,7 @@ class FailedBackupStatusRoute extends PageRouteInfo { /// generated route for /// [FavoritesPage] class FavoritesRoute extends PageRouteInfo { - const FavoritesRoute({List? children}) - : super(FavoritesRoute.name, initialChildren: children); + const FavoritesRoute({List? children}) : super(FavoritesRoute.name, initialChildren: children); static const String name = 'FavoritesRoute'; @@ -643,8 +1255,7 @@ class FavoritesRoute extends PageRouteInfo { /// generated route for /// [FeatInDevPage] class FeatInDevRoute extends PageRouteInfo { - const FeatInDevRoute({List? children}) - : super(FeatInDevRoute.name, initialChildren: children); + const FeatInDevRoute({List? children}) : super(FeatInDevRoute.name, initialChildren: children); static const String name = 'FeatInDevRoute'; @@ -826,8 +1437,7 @@ class HeaderSettingsRoute extends PageRouteInfo { /// generated route for /// [LibraryPage] class LibraryRoute extends PageRouteInfo { - const LibraryRoute({List? children}) - : super(LibraryRoute.name, initialChildren: children); + const LibraryRoute({List? children}) : super(LibraryRoute.name, initialChildren: children); static const String name = 'LibraryRoute'; @@ -842,8 +1452,7 @@ class LibraryRoute extends PageRouteInfo { /// generated route for /// [LocalAlbumsPage] class LocalAlbumsRoute extends PageRouteInfo { - const LocalAlbumsRoute({List? children}) - : super(LocalAlbumsRoute.name, initialChildren: children); + const LocalAlbumsRoute({List? children}) : super(LocalAlbumsRoute.name, initialChildren: children); static const String name = 'LocalAlbumsRoute'; @@ -876,11 +1485,11 @@ class LocalMediaSummaryRoute extends PageRouteInfo { class LocalTimelineRoute extends PageRouteInfo { LocalTimelineRoute({ Key? key, - required String albumId, + required LocalAlbum album, List? children, }) : super( LocalTimelineRoute.name, - args: LocalTimelineRouteArgs(key: key, albumId: albumId), + args: LocalTimelineRouteArgs(key: key, album: album), initialChildren: children, ); @@ -890,29 +1499,28 @@ class LocalTimelineRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return LocalTimelinePage(key: args.key, albumId: args.albumId); + return LocalTimelinePage(key: args.key, album: args.album); }, ); } class LocalTimelineRouteArgs { - const LocalTimelineRouteArgs({this.key, required this.albumId}); + const LocalTimelineRouteArgs({this.key, required this.album}); final Key? key; - final String albumId; + final LocalAlbum album; @override String toString() { - return 'LocalTimelineRouteArgs{key: $key, albumId: $albumId}'; + return 'LocalTimelineRouteArgs{key: $key, album: $album}'; } } /// generated route for /// [LockedPage] class LockedRoute extends PageRouteInfo { - const LockedRoute({List? children}) - : super(LockedRoute.name, initialChildren: children); + const LockedRoute({List? children}) : super(LockedRoute.name, initialChildren: children); static const String name = 'LockedRoute'; @@ -927,8 +1535,7 @@ class LockedRoute extends PageRouteInfo { /// generated route for /// [LoginPage] class LoginRoute extends PageRouteInfo { - const LoginRoute({List? children}) - : super(LoginRoute.name, initialChildren: children); + const LoginRoute({List? children}) : super(LoginRoute.name, initialChildren: children); static const String name = 'LoginRoute'; @@ -943,8 +1550,7 @@ class LoginRoute extends PageRouteInfo { /// generated route for /// [MainTimelinePage] class MainTimelineRoute extends PageRouteInfo { - const MainTimelineRoute({List? children}) - : super(MainTimelineRoute.name, initialChildren: children); + const MainTimelineRoute({List? children}) : super(MainTimelineRoute.name, initialChildren: children); static const String name = 'MainTimelineRoute'; @@ -1196,8 +1802,7 @@ class PartnerDetailRouteArgs { /// generated route for /// [PartnerPage] class PartnerRoute extends PageRouteInfo { - const PartnerRoute({List? children}) - : super(PartnerRoute.name, initialChildren: children); + const PartnerRoute({List? children}) : super(PartnerRoute.name, initialChildren: children); static const String name = 'PartnerRoute'; @@ -1296,8 +1901,7 @@ class PersonResultRouteArgs { /// generated route for /// [PhotosPage] class PhotosRoute extends PageRouteInfo { - const PhotosRoute({List? children}) - : super(PhotosRoute.name, initialChildren: children); + const PhotosRoute({List? children}) : super(PhotosRoute.name, initialChildren: children); static const String name = 'PhotosRoute'; @@ -1396,8 +2000,7 @@ class PlacesCollectionRouteArgs { /// generated route for /// [RecentlyTakenPage] class RecentlyTakenRoute extends PageRouteInfo { - const RecentlyTakenRoute({List? children}) - : super(RecentlyTakenRoute.name, initialChildren: children); + const RecentlyTakenRoute({List? children}) : super(RecentlyTakenRoute.name, initialChildren: children); static const String name = 'RecentlyTakenRoute'; @@ -1409,6 +2012,43 @@ class RecentlyTakenRoute extends PageRouteInfo { ); } +/// generated route for +/// [RemoteAlbumPage] +class RemoteAlbumRoute extends PageRouteInfo { + RemoteAlbumRoute({ + Key? key, + required RemoteAlbum album, + List? children, + }) : super( + RemoteAlbumRoute.name, + args: RemoteAlbumRouteArgs(key: key, album: album), + initialChildren: children, + ); + + static const String name = 'RemoteAlbumRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return RemoteAlbumPage(key: args.key, album: args.album); + }, + ); +} + +class RemoteAlbumRouteArgs { + const RemoteAlbumRouteArgs({this.key, required this.album}); + + final Key? key; + + final RemoteAlbum album; + + @override + String toString() { + return 'RemoteAlbumRouteArgs{key: $key, album: $album}'; + } +} + /// generated route for /// [RemoteMediaSummaryPage] class RemoteMediaSummaryRoute extends PageRouteInfo { @@ -1467,8 +2107,7 @@ class SearchRouteArgs { /// generated route for /// [SettingsPage] class SettingsRoute extends PageRouteInfo { - const SettingsRoute({List? children}) - : super(SettingsRoute.name, initialChildren: children); + const SettingsRoute({List? children}) : super(SettingsRoute.name, initialChildren: children); static const String name = 'SettingsRoute'; @@ -1617,8 +2256,7 @@ class SharedLinkEditRouteArgs { /// generated route for /// [SharedLinkPage] class SharedLinkRoute extends PageRouteInfo { - const SharedLinkRoute({List? children}) - : super(SharedLinkRoute.name, initialChildren: children); + const SharedLinkRoute({List? children}) : super(SharedLinkRoute.name, initialChildren: children); static const String name = 'SharedLinkRoute'; @@ -1633,8 +2271,7 @@ class SharedLinkRoute extends PageRouteInfo { /// generated route for /// [SplashScreenPage] class SplashScreenRoute extends PageRouteInfo { - const SplashScreenRoute({List? children}) - : super(SplashScreenRoute.name, initialChildren: children); + const SplashScreenRoute({List? children}) : super(SplashScreenRoute.name, initialChildren: children); static const String name = 'SplashScreenRoute'; @@ -1649,8 +2286,7 @@ class SplashScreenRoute extends PageRouteInfo { /// generated route for /// [TabControllerPage] class TabControllerRoute extends PageRouteInfo { - const TabControllerRoute({List? children}) - : super(TabControllerRoute.name, initialChildren: children); + const TabControllerRoute({List? children}) : super(TabControllerRoute.name, initialChildren: children); static const String name = 'TabControllerRoute'; @@ -1662,11 +2298,25 @@ class TabControllerRoute extends PageRouteInfo { ); } +/// generated route for +/// [TabShellPage] +class TabShellRoute extends PageRouteInfo { + const TabShellRoute({List? children}) : super(TabShellRoute.name, initialChildren: children); + + static const String name = 'TabShellRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const TabShellPage(); + }, + ); +} + /// generated route for /// [TrashPage] class TrashRoute extends PageRouteInfo { - const TrashRoute({List? children}) - : super(TrashRoute.name, initialChildren: children); + const TrashRoute({List? children}) : super(TrashRoute.name, initialChildren: children); static const String name = 'TrashRoute'; diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart new file mode 100644 index 0000000000..4ed80d9f90 --- /dev/null +++ b/mobile/lib/services/action.service.dart @@ -0,0 +1,236 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/repositories/download.repository.dart'; +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; +import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; +import 'package:immich_mobile/routing/router.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( + ref.watch(assetApiRepositoryProvider), + ref.watch(remoteAssetRepositoryProvider), + ref.watch(localAssetRepository), + ref.watch(driftAlbumApiRepositoryProvider), + ref.watch(remoteAlbumRepository), + ref.watch(assetMediaRepositoryProvider), + ref.watch(downloadRepositoryProvider), + ), +); + +class ActionService { + final AssetApiRepository _assetApiRepository; + final RemoteAssetRepository _remoteAssetRepository; + final DriftLocalAssetRepository _localAssetRepository; + final DriftAlbumApiRepository _albumApiRepository; + final DriftRemoteAlbumRepository _remoteAlbumRepository; + final AssetMediaRepository _assetMediaRepository; + final DownloadRepository _downloadRepository; + + const ActionService( + this._assetApiRepository, + this._remoteAssetRepository, + this._localAssetRepository, + this._albumApiRepository, + this._remoteAlbumRepository, + this._assetMediaRepository, + this._downloadRepository, + ); + + Future shareLink(List remoteIds, BuildContext context) async { + context.pushRoute( + SharedLinkEditRoute( + assetsList: remoteIds, + ), + ); + } + + Future favorite(List remoteIds) async { + await _assetApiRepository.updateFavorite(remoteIds, true); + await _remoteAssetRepository.updateFavorite(remoteIds, true); + } + + Future unFavorite(List remoteIds) async { + await _assetApiRepository.updateFavorite(remoteIds, false); + await _remoteAssetRepository.updateFavorite(remoteIds, false); + } + + Future archive(List remoteIds) async { + await _assetApiRepository.updateVisibility( + remoteIds, + AssetVisibilityEnum.archive, + ); + await _remoteAssetRepository.updateVisibility( + remoteIds, + AssetVisibility.archive, + ); + } + + Future unArchive(List remoteIds) async { + await _assetApiRepository.updateVisibility( + remoteIds, + AssetVisibilityEnum.timeline, + ); + await _remoteAssetRepository.updateVisibility( + remoteIds, + AssetVisibility.timeline, + ); + } + + Future moveToLockFolder( + List remoteIds, + List localIds, + ) async { + await _assetApiRepository.updateVisibility( + remoteIds, + AssetVisibilityEnum.locked, + ); + await _remoteAssetRepository.updateVisibility( + remoteIds, + AssetVisibility.locked, + ); + + // Ask user if they want to delete local copies + if (localIds.isNotEmpty) { + final deletedIds = await _assetMediaRepository.deleteAll(localIds); + + if (deletedIds.isNotEmpty) { + await _localAssetRepository.delete(deletedIds); + } + } + } + + Future removeFromLockFolder(List remoteIds) async { + await _assetApiRepository.updateVisibility( + remoteIds, + AssetVisibilityEnum.timeline, + ); + await _remoteAssetRepository.updateVisibility( + remoteIds, + AssetVisibility.timeline, + ); + } + + Future trash(List remoteIds) async { + await _assetApiRepository.delete(remoteIds, false); + await _remoteAssetRepository.trash(remoteIds); + } + + Future restoreTrash(List ids) async { + await _assetApiRepository.restoreTrash(ids); + await _remoteAssetRepository.restoreTrash(ids); + } + + Future trashRemoteAndDeleteLocal(List remoteIds, List localIds) async { + await _assetApiRepository.delete(remoteIds, false); + await _remoteAssetRepository.trash(remoteIds); + + if (localIds.isNotEmpty) { + final deletedIds = await _assetMediaRepository.deleteAll(localIds); + + if (deletedIds.isNotEmpty) { + await _localAssetRepository.delete(deletedIds); + } + } + } + + Future deleteRemoteAndLocal( + List remoteIds, + List localIds, + ) async { + await _assetApiRepository.delete(remoteIds, true); + await _remoteAssetRepository.delete(remoteIds); + + if (localIds.isNotEmpty) { + final deletedIds = await _assetMediaRepository.deleteAll(localIds); + + if (deletedIds.isNotEmpty) { + await _localAssetRepository.delete(deletedIds); + } + } + } + + Future deleteLocal(List localIds) async { + final deletedIds = await _assetMediaRepository.deleteAll(localIds); + if (deletedIds.isNotEmpty) { + await _localAssetRepository.delete(deletedIds); + return deletedIds.length; + } + + return 0; + } + + Future editLocation( + List remoteIds, + BuildContext context, + ) async { + maplibre.LatLng? initialLatLng; + if (remoteIds.length == 1) { + final exif = await _remoteAssetRepository.getExif(remoteIds[0]); + + if (exif?.latitude != null && exif?.longitude != null) { + initialLatLng = maplibre.LatLng(exif!.latitude!, exif.longitude!); + } + } + + final location = await showLocationPicker( + context: context, + initialLatLng: initialLatLng, + ); + + if (location == null) { + return false; + } + + await _assetApiRepository.updateLocation( + remoteIds, + location, + ); + await _remoteAssetRepository.updateLocation( + remoteIds, + location, + ); + + return true; + } + + Future removeFromAlbum(List remoteIds, String albumId) async { + int removedCount = 0; + final result = await _albumApiRepository.removeAssets(albumId, remoteIds); + + if (result.removed.isNotEmpty) { + removedCount = await _remoteAlbumRepository.removeAssets(albumId, result.removed); + } + + return removedCount; + } + + Future stack(String userId, List remoteIds) async { + final stack = await _assetApiRepository.stack(remoteIds); + await _remoteAssetRepository.stack(userId, stack); + } + + Future unStack(List stackIds) async { + await _remoteAssetRepository.unStack(stackIds); + await _assetApiRepository.unStack(stackIds); + } + + Future shareAssets(List assets) { + return _assetMediaRepository.shareAssets(assets); + } + + Future> downloadAll(List assets) { + return _downloadRepository.downloadAllAssets(assets); + } +} diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index bf67f1c24c..308b9acc44 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -11,11 +11,7 @@ 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/interfaces/album.interface.dart'; -import 'package:immich_mobile/interfaces/asset.interface.dart'; -import 'package:immich_mobile/interfaces/backup_album.interface.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'; @@ -46,9 +42,9 @@ class AlbumService { final SyncService _syncService; final UserService _userService; final EntityService _entityService; - final IAlbumRepository _albumRepository; - final IAssetRepository _assetRepository; - final IBackupAlbumRepository _backupAlbumRepository; + final AlbumRepository _albumRepository; + final AssetRepository _assetRepository; + final BackupAlbumRepository _backupAlbumRepository; final AlbumMediaRepository _albumMediaRepository; final AlbumApiRepository _albumApiRepository; final Logger _log = Logger('AlbumService'); @@ -79,12 +75,8 @@ class AlbumService { 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()), + _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"); @@ -129,8 +121,7 @@ class AlbumService { onDevice.removeWhere((album) => !selectedIds.contains(album.localId)); _log.info("'Recents' is not selected, keeping only selected albums"); } - changes = - await _syncService.syncLocalAlbumAssetsToDb(onDevice, excludedAssets); + changes = await _syncService.syncLocalAlbumAssetsToDb(onDevice, excludedAssets); _log.info("Syncing completed. Changes: $changes"); } finally { _localCompleter.complete(changes); @@ -144,14 +135,10 @@ class AlbumService { Set excludedAlbumIds, ) async { final Set result = HashSet(); - for (final batchAlbums in albums - .where((album) => excludedAlbumIds.contains(album.localId)) - .slices(5)) { + 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)), + (album) => _albumMediaRepository.getAssetIds(album.localId!).then((assetIds) => result.addAll(assetIds)), ) .wait; } @@ -247,9 +234,8 @@ class AlbumService { assets.map((asset) => asset.remoteId!), ); - final List addedAssets = result.added - .map((id) => assets.firstWhere((asset) => asset.remoteId == id)) - .toList(); + final List addedAssets = + result.added.map((id) => assets.firstWhere((asset) => asset.remoteId == id)).toList(); await _updateAssets(album.id, add: addedAssets); @@ -299,8 +285,7 @@ class AlbumService { await _albumApiRepository.delete(album.remoteId!); } if (album.shared) { - final foreignAssets = - await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); + final foreignAssets = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); await _albumRepository.delete(album.id); final List albums = await _albumRepository.getAll(shared: true); @@ -310,8 +295,7 @@ class AlbumService { await _assetRepository.getByAlbum(album, notOwnedBy: [userId]), ); } - final List idsToRemove = - _syncService.sharedAssetsToRemove(foreignAssets, existing); + final List idsToRemove = _syncService.sharedAssetsToRemove(foreignAssets, existing); if (idsToRemove.isNotEmpty) { await _assetRepository.deleteByIds(idsToRemove); } @@ -344,8 +328,7 @@ class AlbumService { album.remoteId!, assets.map((asset) => asset.remoteId!), ); - final toRemove = result.removed - .map((id) => assets.firstWhere((asset) => asset.remoteId == id)); + final toRemove = result.removed.map((id) => assets.firstWhere((asset) => asset.remoteId == id)); await _updateAssets(album.id, remove: toRemove.toList()); return true; } catch (e) { @@ -382,8 +365,7 @@ class AlbumService { List userIds, ) async { try { - final updatedAlbum = - await _albumApiRepository.addUsers(album.remoteId!, userIds); + final updatedAlbum = await _albumApiRepository.addUsers(album.remoteId!, userIds); album.sharedUsers.addAll(updatedAlbum.remoteUsers); album.shared = true; @@ -489,6 +471,10 @@ class AlbumService { return _albumRepository.get(id); } + Future getAlbumByRemoteId(String remoteId) { + return _albumRepository.getByRemoteId(remoteId); + } + Stream watchAlbum(int id) { return _albumRepository.watchAlbum(id); } @@ -502,8 +488,7 @@ class AlbumService { Future updateSortOrder(Album album, SortOrder order) async { try { - final updateAlbum = - await _albumApiRepository.update(album.remoteId!, sortOrder: order); + final updateAlbum = await _albumApiRepository.update(album.remoteId!, sortOrder: order); album.sortOrder = updateAlbum.sortOrder; return _albumRepository.update(album); diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index fe007a2aab..b2e73c1b28 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -178,13 +178,11 @@ class ApiService implements Authentication { if (Platform.isIOS) { final iosInfo = await deviceInfoPlugin.iosInfo; - authenticationApi.apiClient - .addDefaultHeader('deviceModel', iosInfo.utsname.machine); + authenticationApi.apiClient.addDefaultHeader('deviceModel', iosInfo.utsname.machine); authenticationApi.apiClient.addDefaultHeader('deviceType', 'iOS'); } else if (Platform.isAndroid) { final androidInfo = await deviceInfoPlugin.androidInfo; - authenticationApi.apiClient - .addDefaultHeader('deviceModel', androidInfo.model); + authenticationApi.apiClient.addDefaultHeader('deviceModel', androidInfo.model); authenticationApi.apiClient.addDefaultHeader('deviceType', 'Android'); } else { authenticationApi.apiClient.addDefaultHeader('deviceModel', 'Unknown'); diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index b6c675b636..cefc52385a 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -88,8 +88,10 @@ enum AppSettingsEnum { photoManagerCustomFilter( StoreKey.photoManagerCustomFilter, null, - false, + true, ), + betaTimeline(StoreKey.betaTimeline, null, false), + enableBackup(StoreKey.enableBackup, null, false), ; const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); @@ -100,6 +102,7 @@ enum AppSettingsEnum { } class AppSettingsService { + const AppSettingsService(); T getSetting(AppSettingsEnum setting) { return Store.get(setting.storeKey, setting.defaultValue); } diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index e57edde55e..e9318dd0bf 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -4,17 +4,12 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; import 'package:immich_mobile/domain/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/interfaces/asset.interface.dart'; -import 'package:immich_mobile/interfaces/asset_api.interface.dart'; -import 'package:immich_mobile/interfaces/asset_media.interface.dart'; -import 'package:immich_mobile/interfaces/backup_album.interface.dart'; -import 'package:immich_mobile/interfaces/etag.interface.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'; @@ -50,18 +45,18 @@ final assetServiceProvider = Provider( ); class AssetService { - final IAssetApiRepository _assetApiRepository; - final IAssetRepository _assetRepository; - final IExifInfoRepository _exifInfoRepository; + final AssetApiRepository _assetApiRepository; + final AssetRepository _assetRepository; + final IsarExifRepository _exifInfoRepository; final IsarUserRepository _isarUserRepository; - final IETagRepository _etagRepository; - final IBackupAlbumRepository _backupRepository; + final ETagRepository _etagRepository; + final BackupAlbumRepository _backupRepository; final ApiService _apiService; final SyncService _syncService; final BackupService _backupService; final AlbumService _albumService; final UserService _userService; - final IAssetMediaRepository _assetMediaRepository; + final AssetMediaRepository _assetMediaRepository; final log = Logger('AssetService'); AssetService( @@ -83,11 +78,8 @@ class AssetService { /// 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 List syncedUsers = + syncedUserIds.isEmpty ? [] : (await _isarUserRepository.getByUserIds(syncedUserIds)).nonNulls.toList(); final Stopwatch sw = Stopwatch()..start(); final bool changes = await _syncService.syncRemoteAssetsToDb( users: syncedUsers, @@ -99,8 +91,10 @@ class AssetService { } /// Returns `(null, null)` if changes are invalid -> requires full sync - Future<(List? toUpsert, List? toDelete)> - _getRemoteAssetChanges(List users, DateTime since) async { + Future<(List? toUpsert, List? toDelete)> _getRemoteAssetChanges( + List users, + DateTime since, + ) async { final dto = AssetDeltaSyncDto( updatedAfter: since, userIds: users.map((e) => e.id).toList(), @@ -117,8 +111,7 @@ class AssetService { String remoteId, ) async { try { - final AssetResponseDto? dto = - await _apiService.assetsApi.getAssetInfo(remoteId); + final AssetResponseDto? dto = await _apiService.assetsApi.getAssetInfo(remoteId); return dto?.people; } catch (error, stack) { @@ -147,8 +140,7 @@ class AssetService { userId: user.id, ); log.fine("Requesting $chunkSize assets from $lastId"); - final List? assets = - await _apiService.syncApi.getFullSyncForUser(dto); + 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}", @@ -177,8 +169,7 @@ class AssetService { a.exifInfo = newExif; if (newExif != a.exifInfo) { if (a.isInDb) { - await _assetRepository - .transaction(() => _assetRepository.update(a)); + await _assetRepository.transaction(() => _assetRepository.update(a)); } else { debugPrint("[loadExif] parameter Asset is not from DB!"); } @@ -235,16 +226,13 @@ class AssetService { await updateAssets( assets, UpdateAssetDto( - visibility: - isArchived ? AssetVisibility.archive : AssetVisibility.timeline, + visibility: isArchived ? AssetVisibility.archive : AssetVisibility.timeline, ), ); for (var element in assets) { element.isArchived = isArchived; - element.visibility = isArchived - ? AssetVisibilityEnum.archive - : AssetVisibilityEnum.timeline; + element.visibility = isArchived ? AssetVisibilityEnum.archive : AssetVisibilityEnum.timeline; } await _syncService.upsertAssetsWithExif(assets); @@ -268,8 +256,7 @@ class AssetService { for (var element in assets) { element.fileCreatedAt = DateTime.parse(updatedDt); - element.exifInfo = element.exifInfo - ?.copyWith(dateTimeOriginal: DateTime.parse(updatedDt)); + element.exifInfo = element.exifInfo?.copyWith(dateTimeOriginal: DateTime.parse(updatedDt)); } await _syncService.upsertAssetsWithExif(assets); @@ -312,10 +299,8 @@ class AssetService { Future syncUploadedAssetToAlbums() async { try { - final selectedAlbums = - await _backupRepository.getAllBySelection(BackupSelection.select); - final excludedAlbums = - await _backupRepository.getAllBySelection(BackupSelection.exclude); + final selectedAlbums = await _backupRepository.getAllBySelection(BackupSelection.select); + final excludedAlbums = await _backupRepository.getAllBySelection(BackupSelection.exclude); final candidates = await _backupService.buildUploadCandidates( selectedAlbums, @@ -380,8 +365,7 @@ class AssetService { var exifInfo = await _exifInfoRepository.get(localExifId); if (exifInfo != null) { - await _exifInfoRepository - .update(exifInfo.copyWith(description: description)); + await _exifInfoRepository.update(exifInfo.copyWith(description: description)); } } } @@ -434,22 +418,16 @@ class AssetService { // Delete files from local gallery final candidates = assets.where((asset) => asset.isLocal); - final deletedIds = await _assetMediaRepository - .deleteAll(candidates.map((asset) => asset.localId!).toList()); + 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(); + 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) { + final updatedAssets = assets.where((asset) => asset.storage == AssetState.merged).map((asset) { asset.localId = null; return asset; }).toList(); @@ -478,9 +456,7 @@ class AssetService { /// Update asset info bassed on the deletion type. final payload = shouldDeletePermanently - ? assets - .where((asset) => asset.storage == AssetState.merged) - .map((asset) { + ? assets.where((asset) => asset.storage == AssetState.merged).map((asset) { asset.remoteId = null; asset.visibility = AssetVisibilityEnum.timeline; return asset; @@ -494,10 +470,8 @@ class AssetService { await _assetRepository.updateAll(payload.toList()); if (shouldDeletePermanently) { - final remoteAssetIds = assets - .where((asset) => asset.storage == AssetState.remote) - .map((asset) => asset.id) - .toList(); + final remoteAssetIds = + assets.where((asset) => asset.storage == AssetState.remote).map((asset) => asset.id).toList(); await _assetRepository.deleteByIds(remoteAssetIds); } }); @@ -554,4 +528,9 @@ class AssetService { await _assetRepository.updateAll(updatedAssets); } + + Future getAssetByRemoteId(String remoteId) async { + final assets = await _assetRepository.getAllByRemoteId([remoteId]); + return assets.isNotEmpty ? assets.first : null; + } } diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index 41709b714c..e8c4c5e97e 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -5,15 +5,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/interfaces/auth.interface.dart'; -import 'package:immich_mobile/interfaces/auth_api.interface.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/network.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -25,16 +25,17 @@ final authServiceProvider = Provider( ref.watch(apiServiceProvider), ref.watch(networkServiceProvider), ref.watch(backgroundSyncProvider), + ref.watch(appSettingsServiceProvider), ), ); class AuthService { - final IAuthApiRepository _authApiRepository; - final IAuthRepository _authRepository; + final AuthApiRepository _authApiRepository; + final AuthRepository _authRepository; final ApiService _apiService; final NetworkService _networkService; final BackgroundSyncManager _backgroundSyncManager; - + final AppSettingsService _appSettingsService; final _log = Logger("AuthService"); AuthService( @@ -43,6 +44,7 @@ class AuthService { this._apiService, this._networkService, this._backgroundSyncManager, + this._appSettingsService, ); /// Validates the provided server URL by resolving and setting the endpoint. @@ -108,6 +110,11 @@ class AuthService { await clearLocalData().catchError((error, stackTrace) { _log.severe("Error clearing local data", error, stackTrace); }); + + await _appSettingsService.setSetting( + AppSettingsEnum.enableBackup, + false, + ); } } diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index 4c4a9b1cff..5302df49ce 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -14,7 +14,6 @@ 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/interfaces/backup_album.interface.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'; @@ -40,10 +39,8 @@ 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 MethodChannel _foregroundChannel = MethodChannel('immich/foregroundChannel'); + static const MethodChannel _backgroundChannel = MethodChannel('immich/backgroundChannel'); static const notifyInterval = Duration(milliseconds: 400); bool _isBackgroundInitialized = false; CancellationToken? _cancellationToken; @@ -57,8 +54,7 @@ class BackgroundService { int _assetsToUploadCount = 0; String _lastPrintedDetailContent = ""; String? _lastPrintedDetailTitle; - late final ThrottleProgressUpdate _throttledNotifiy = - ThrottleProgressUpdate(_updateProgress, notifyInterval); + late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval); late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate(_updateDetailProgress, notifyInterval); @@ -75,10 +71,8 @@ class BackgroundService { 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]); + 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; @@ -134,8 +128,7 @@ class BackgroundService { return true; } try { - return await _foregroundChannel - .invokeMethod('isIgnoringBatteryOptimizations'); + return await _foregroundChannel.invokeMethod('isIgnoringBatteryOptimizations'); } catch (error) { return false; } @@ -184,8 +177,7 @@ class BackgroundService { }) async { try { if (_isBackgroundInitialized && _errorGracePeriodExceeded) { - return await _backgroundChannel - .invokeMethod('showError', [title, content, individualTag]); + return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]); } } catch (error) { debugPrint("[_showErrorNotification] failed to communicate with plugin"); @@ -241,8 +233,7 @@ class BackgroundService { final bs = tempRp.asBroadcastStream(); while (_wantsLockTime == lockTime) { other.send(tempSp); - final dynamic answer = await bs.first - .timeout(const Duration(seconds: 3), onTimeout: () => null); + final dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null); if (_wantsLockTime != lockTime) { break; } @@ -258,8 +249,7 @@ class BackgroundService { } else if (answer == false) { // other isolate is still active } - final dynamic isFinished = await bs.first - .timeout(const Duration(seconds: 3), onTimeout: () => false); + final dynamic isFinished = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => false); if (isFinished == true) { break; } @@ -359,9 +349,7 @@ class BackgroundService { ); HttpSSLOptions.apply(); - ref - .read(apiServiceProvider) - .setAccessToken(Store.get(StoreKey.accessToken)); + ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken)); await ref.read(authServiceProvider).setOpenApiServiceEndpoint(); if (kDebugMode) { debugPrint( @@ -369,12 +357,8 @@ class BackgroundService { ); } - final selectedAlbums = await ref - .read(backupAlbumRepositoryProvider) - .getAllBySelection(BackupSelection.select); - final excludedAlbums = await ref - .read(backupAlbumRepositoryProvider) - .getAllBySelection(BackupSelection.exclude); + final selectedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.select); + final excludedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.exclude); if (selectedAlbums.isEmpty) { return true; } @@ -393,9 +377,7 @@ class BackgroundService { final backupAlbums = [...selectedAlbums, ...excludedAlbums]; backupAlbums.sortBy((e) => e.id); - final dbAlbums = await ref - .read(backupAlbumRepositoryProvider) - .getAll(sort: BackupAlbumSort.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 @@ -404,9 +386,7 @@ class BackgroundService { 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; + a.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; toUpsert.add(a); return true; }, @@ -420,9 +400,7 @@ class BackgroundService { return false; } // Android should check for new assets added while performing backup - } while (Platform.isAndroid && - true == - await _backgroundChannel.invokeMethod("hasContentChanged")); + } while (Platform.isAndroid && true == await _backgroundChannel.invokeMethod("hasContentChanged")); return true; } @@ -433,10 +411,8 @@ class BackgroundService { List excludedAlbums, ) async { _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService); - final bool notifyTotalProgress = settingsService - .getSetting(AppSettingsEnum.backgroundBackupTotalProgress); - final bool notifySingleProgress = settingsService - .getSetting(AppSettingsEnum.backgroundBackupSingleProgress); + final bool notifyTotalProgress = settingsService.getSetting(AppSettingsEnum.backgroundBackupTotalProgress); + final bool notifySingleProgress = settingsService.getSetting(AppSettingsEnum.backgroundBackupSingleProgress); if (_canceledBySystem) { return false; @@ -490,10 +466,8 @@ class BackgroundService { onSuccess: (result) => _onAssetUploaded( shouldNotify: notifyTotalProgress, ), - onProgress: (bytes, totalBytes) => - _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress), - onCurrentAsset: (asset) => - _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress), + onProgress: (bytes, totalBytes) => _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress), + onCurrentAsset: (asset) => _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress), onError: _onBackupError, isBackground: true, ); @@ -528,8 +502,7 @@ class BackgroundService { } void _updateDetailProgress(String? title, int progress, int total) { - final String msg = - total > 0 ? humanReadableBytesProgress(progress, 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; @@ -558,8 +531,8 @@ class BackgroundService { void _onBackupError(ErrorUploadAsset errorAssetInfo) { _showErrorNotification( - title: "backup_background_service_upload_failure_notification" - .tr(namedArgs: {'filename': errorAssetInfo.fileName}), + title: + "backup_background_service_upload_failure_notification".tr(namedArgs: {'filename': errorAssetInfo.fileName}), individualTag: errorAssetInfo.id, ); } @@ -572,16 +545,14 @@ class BackgroundService { return; } - _throttledDetailNotify.title = - "backup_background_service_current_upload_notification" - .tr(namedArgs: {'filename': currentUploadAsset.fileName}); + _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); + final int value = appSettingsService.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod); if (value == 0) { return true; } else if (value == 5) { diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index d394b4773c..ea332dd1f9 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -11,9 +11,6 @@ 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/interfaces/asset.interface.dart'; -import 'package:immich_mobile/interfaces/asset_media.interface.dart'; -import 'package:immich_mobile/interfaces/file_media.interface.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'; @@ -52,9 +49,9 @@ class BackupService { final AppSettingsService _appSetting; final AlbumService _albumService; final AlbumMediaRepository _albumMediaRepository; - final IFileMediaRepository _fileMediaRepository; - final IAssetRepository _assetRepository; - final IAssetMediaRepository _assetMediaRepository; + final FileMediaRepository _fileMediaRepository; + final AssetRepository _assetRepository; + final AssetMediaRepository _assetMediaRepository; BackupService( this._apiService, @@ -77,8 +74,7 @@ class BackupService { } } - Future _saveDuplicatedAssetIds(List deviceAssetIds) => - _assetRepository.transaction( + Future _saveDuplicatedAssetIds(List deviceAssetIds) => _assetRepository.transaction( () => _assetRepository.upsertDuplicatedAssets(deviceAssetIds), ); @@ -130,8 +126,7 @@ class BackupService { continue; } - if (useTimeFilter && - localAlbum.modifiedAt.isBefore(backupAlbum.lastBackup)) { + if (useTimeFilter && localAlbum.modifiedAt.isBefore(backupAlbum.lastBackup)) { continue; } final List assets; @@ -192,8 +187,7 @@ class BackupService { final Set existing = {}; try { final String deviceId = Store.get(StoreKey.deviceId); - final CheckExistingAssetsResponseDto? duplicates = - await _apiService.assetsApi.checkExistingAssets( + final CheckExistingAssetsResponseDto? duplicates = await _apiService.assetsApi.checkExistingAssets( CheckExistingAssetsDto( deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(), deviceId: deviceId, @@ -218,8 +212,7 @@ class BackupService { } Future _checkPermissions() async { - if (Platform.isAndroid && - !(await pm.Permission.accessMediaLocation.status).isGranted) { + 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. " @@ -258,8 +251,7 @@ class BackupService { required void Function(CurrentUploadAsset asset) onCurrentAsset, required void Function(ErrorUploadAsset error) onError, }) async { - final bool isIgnoreIcloudAssets = - _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); + 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); @@ -282,8 +274,7 @@ class BackupService { File? livePhotoFile; try { - final isAvailableLocally = - await asset.local!.isLocallyAvailable(isOrigin: true); + final isAvailableLocally = await asset.local!.isLocallyAvailable(isOrigin: true); // Handle getting files from iCloud if (!isAvailableLocally && Platform.isIOS) { @@ -295,17 +286,14 @@ class BackupService { onCurrentAsset( CurrentUploadAsset( id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt.year == 1970 - ? asset.fileModifiedAt - : asset.fileCreatedAt, + 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); + file = await asset.local!.loadFile(progressHandler: pmProgressHandler); if (asset.local!.isLivePhoto) { livePhotoFile = await asset.local!.loadFile( withSubtype: true, @@ -313,18 +301,15 @@ class BackupService { ); } } else { - file = - await asset.local!.originFile.timeout(const Duration(seconds: 5)); + file = await asset.local!.originFile.timeout(const Duration(seconds: 5)); if (asset.local!.isLivePhoto) { - livePhotoFile = await asset.local!.originFileWithSubtype - .timeout(const Duration(seconds: 5)); + livePhotoFile = await asset.local!.originFileWithSubtype.timeout(const Duration(seconds: 5)); } } if (file != null) { - String? originalFileName = - await _assetMediaRepository.getOriginalFilename(asset.localId!); + String? originalFileName = await _assetMediaRepository.getOriginalFilename(asset.localId!); originalFileName ??= asset.fileName; if (asset.local!.isLivePhoto) { @@ -352,10 +337,8 @@ class BackupService { baseRequest.headers.addAll(ApiService.getRequestHeaders()); 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['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); @@ -363,9 +346,7 @@ class BackupService { onCurrentAsset( CurrentUploadAsset( id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt.year == 1970 - ? asset.fileModifiedAt - : asset.fileCreatedAt, + fileCreatedAt: asset.fileCreatedAt.year == 1970 ? asset.fileModifiedAt : asset.fileCreatedAt, fileName: originalFileName, fileType: _getAssetType(asset.type), fileSize: file.lengthSync(), @@ -392,8 +373,7 @@ class BackupService { cancellationToken: cancelToken, ); - final responseBody = - jsonDecode(await response.stream.bytesToString()); + final responseBody = jsonDecode(await response.stream.bytesToString()); if (![200, 201].contains(response.statusCode)) { final error = responseBody; diff --git a/mobile/lib/services/backup_album.service.dart b/mobile/lib/services/backup_album.service.dart index 8030d66937..ef9d1031de 100644 --- a/mobile/lib/services/backup_album.service.dart +++ b/mobile/lib/services/backup_album.service.dart @@ -1,6 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/repositories/backup.repository.dart'; final backupAlbumServiceProvider = Provider((ref) { @@ -8,9 +7,9 @@ final backupAlbumServiceProvider = Provider((ref) { }); class BackupAlbumService { - final IBackupAlbumRepository _backupAlbumRepository; + final BackupAlbumRepository _backupAlbumRepository; - BackupAlbumService(this._backupAlbumRepository); + const BackupAlbumService(this._backupAlbumRepository); Future> getAll({BackupAlbumSort? sort}) { return _backupAlbumRepository.getAll(sort: sort); diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart index 408ac51d74..2f61a125ea 100644 --- a/mobile/lib/services/backup_verification.service.dart +++ b/mobile/lib/services/backup_verification.service.dart @@ -5,15 +5,13 @@ 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/interfaces/exif.interface.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/interfaces/asset.interface.dart'; -import 'package:immich_mobile/interfaces/file_media.interface.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'; @@ -25,9 +23,9 @@ import 'package:immich_mobile/utils/diff.dart'; /// Finds duplicates originating from missing EXIF information class BackupVerificationService { final UserService _userService; - final IFileMediaRepository _fileMediaRepository; - final IAssetRepository _assetRepository; - final IExifInfoRepository _exifInfoRepository; + final FileMediaRepository _fileMediaRepository; + final AssetRepository _assetRepository; + final IsarExifRepository _exifInfoRepository; const BackupVerificationService( this._userService, @@ -123,7 +121,7 @@ class BackupVerificationService { String auth, String endpoint, RootIsolateToken rootIsolateToken, - IFileMediaRepository fileMediaRepository, + FileMediaRepository fileMediaRepository, }) tuple, ) async { assert(tuple.deleteCandidates.length == tuple.originals.length); @@ -183,10 +181,8 @@ class BackupVerificationService { // 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 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); @@ -200,9 +196,7 @@ class BackupVerificationService { 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; + final start = bytes.length < headerLength * 2 ? (bytes.length ~/ (4 * 8)) * 8 : headerLength; return bytes.buffer.asUint64List(start); } diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart new file mode 100644 index 0000000000..c08eacd0e3 --- /dev/null +++ b/mobile/lib/services/deep_link.service.dart @@ -0,0 +1,200 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:immich_mobile/domain/services/asset.service.dart' as beta_asset_service; +import 'package:immich_mobile/domain/services/memory.service.dart'; +import 'package:immich_mobile/domain/services/remote_album.service.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/providers/album/current_album.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.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'; +import 'package:riverpod_annotation/riverpod_annotation.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(currentRemoteAlbumProvider.notifier), + ref.watch(remoteAlbumServiceProvider), + ref.watch(driftMemoryServiceProvider), + ), +); + +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 CurrentAlbumNotifier _betaCurrentAlbumNotifier; + final RemoteAlbumService _betaRemoteAlbumService; + final DriftMemoryService _betaMemoryServiceProvider; + + const DeepLinkService( + this._memoryService, + this._assetService, + this._albumService, + this._currentAsset, + this._currentAlbum, + this._betaTimelineFactory, + this._betaAssetService, + this._betaCurrentAlbumNotifier, + this._betaRemoteAlbumService, + this._betaMemoryServiceProvider, + ); + + DeepLink _handleColdStart(PageRouteInfo route, bool isColdStart) { + return DeepLink([ + // we need something to segue back to if the app was cold started + // TODO: use MainTimelineRoute this when beta is default + + if (isColdStart) (Store.isBetaTimelineEnabled) ? const MainTimelineRoute() : const PhotosRoute(), + route, + ]); + } + + Future handleScheme(PlatformDeepLink link, bool isColdStart) async { + // get everything after the scheme, since Uri cannot parse path + final intent = link.uri.host; + final queryParams = link.uri.queryParameters; + + PageRouteInfo? deepLinkRoute = switch (intent) { + "memory" => await _buildMemoryDeepLink(queryParams['id'] ?? ''), + "asset" => await _buildAssetDeepLink(queryParams['id'] ?? ''), + "album" => await _buildAlbumDeepLink(queryParams['id'] ?? ''), + _ => null, + }; + + // Deep link resolution failed, safely handle it based on the app state + if (deepLinkRoute == null) { + if (isColdStart) { + return DeepLink.defaultPath; + } + + return DeepLink.none; + } + + return _handleColdStart(deepLinkRoute, isColdStart); + } + + Future handleMyImmichApp( + PlatformDeepLink link, + bool isColdStart, + ) async { + final path = link.uri.path; + + const uuidRegex = r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'; + final assetRegex = RegExp('/photos/($uuidRegex)'); + final albumRegex = RegExp('/albums/($uuidRegex)'); + + PageRouteInfo? deepLinkRoute; + if (assetRegex.hasMatch(path)) { + final assetId = assetRegex.firstMatch(path)?.group(1) ?? ''; + deepLinkRoute = await _buildAssetDeepLink(assetId); + } else if (albumRegex.hasMatch(path)) { + final albumId = albumRegex.firstMatch(path)?.group(1) ?? ''; + deepLinkRoute = await _buildAlbumDeepLink(albumId); + } + + // Deep link resolution failed, safely handle it based on the app state + if (deepLinkRoute == null) { + if (isColdStart) return DeepLink.defaultPath; + return DeepLink.none; + } + + return _handleColdStart(deepLinkRoute, isColdStart); + } + + Future _buildMemoryDeepLink(String memoryId) async { + if (Store.isBetaTimelineEnabled) { + final memory = await _betaMemoryServiceProvider.get(memoryId); + + if (memory == null) { + return null; + } + + return DriftMemoryRoute(memories: [memory], memoryIndex: 0); + } else { + // TODO: Remove this when beta is default + final memory = await _memoryService.getMemoryById(memoryId); + + if (memory == null) { + return null; + } + + return MemoryRoute(memories: [memory], memoryIndex: 0); + } + } + + Future _buildAssetDeepLink(String assetId) async { + if (Store.isBetaTimelineEnabled) { + final asset = await _betaAssetService.getRemoteAsset(assetId); + if (asset == null) { + return null; + } + + return AssetViewerRoute( + initialIndex: 0, + timelineService: _betaTimelineFactory.fromAssets([asset]), + ); + } 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; + } + + _betaCurrentAlbumNotifier.setAlbum(album); + 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); + } + } +} diff --git a/mobile/lib/services/device.service.dart b/mobile/lib/services/device.service.dart index 393274608b..50a0d93b24 100644 --- a/mobile/lib/services/device.service.dart +++ b/mobile/lib/services/device.service.dart @@ -3,10 +3,10 @@ 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) => DeviceService()); +final deviceServiceProvider = Provider((ref) => const DeviceService()); class DeviceService { - DeviceService(); + const DeviceService(); createDeviceId() { return FlutterUdid.consistentUdid; diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart index d8120a384c..a540ca103f 100644 --- a/mobile/lib/services/download.service.dart +++ b/mobile/lib/services/download.service.dart @@ -3,15 +3,14 @@ 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/interfaces/file_media.interface.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:immich_mobile/utils/download.dart'; import 'package:logging/logging.dart'; final downloadServiceProvider = Provider( @@ -23,7 +22,7 @@ final downloadServiceProvider = Provider( class DownloadService { final DownloadRepository _downloadRepository; - final IFileMediaRepository _fileMediaRepository; + final FileMediaRepository _fileMediaRepository; final Logger _log = Logger("DownloadService"); void Function(TaskStatusUpdate)? onImageDownloadStatus; void Function(TaskStatusUpdate)? onVideoDownloadStatus; @@ -36,8 +35,7 @@ class DownloadService { ) { _downloadRepository.onImageDownloadStatus = _onImageDownloadCallback; _downloadRepository.onVideoDownloadStatus = _onVideoDownloadCallback; - _downloadRepository.onLivePhotoDownloadStatus = - _onLivePhotoDownloadCallback; + _downloadRepository.onLivePhotoDownloadStatus = _onLivePhotoDownloadCallback; _downloadRepository.onTaskProgress = _onTaskProgressCallback; } @@ -109,10 +107,8 @@ class DownloadService { return false; } - final imageRecord = - _findTaskRecord(records, livePhotosId, LivePhotosPart.image); - final videoRecord = - _findTaskRecord(records, livePhotosId, LivePhotosPart.video); + final imageRecord = _findTaskRecord(records, livePhotosId, LivePhotosPart.image); + final videoRecord = _findTaskRecord(records, livePhotosId, LivePhotosPart.video); final imageFilePath = await imageRecord.task.filePath(); final videoFilePath = await videoRecord.task.filePath(); @@ -127,8 +123,7 @@ class DownloadService { } on PlatformException catch (error, stack) { // Handle saving MotionPhotos on iOS if (error.code == 'PHPhotosErrorDomain (-1)') { - final result = await _fileMediaRepository - .saveImageWithFile(imageFilePath, title: task.filename); + final result = await _fileMediaRepository.saveImageWithFile(imageFilePath, title: task.filename); return result != null; } _log.severe("Error saving live photo", error, stack); @@ -159,8 +154,7 @@ class DownloadService { } Future> downloadAll(List assets) async { - return await _downloadRepository - .downloadAll(assets.expand(_createDownloadTasks).toList()); + return await _downloadRepository.downloadAll(assets.expand(_createDownloadTasks).toList()); } Future download(Asset asset) async { @@ -174,7 +168,7 @@ class DownloadService { _buildDownloadTask( asset.remoteId!, asset.fileName, - group: downloadGroupLivePhoto, + group: kDownloadGroupLivePhoto, metadata: LivePhotosMetadata( part: LivePhotosPart.image, id: asset.remoteId!, @@ -182,10 +176,8 @@ class DownloadService { ), _buildDownloadTask( asset.livePhotoVideoId!, - asset.fileName - .toUpperCase() - .replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'), - group: downloadGroupLivePhoto, + asset.fileName.toUpperCase().replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'), + group: kDownloadGroupLivePhoto, metadata: LivePhotosMetadata( part: LivePhotosPart.video, id: asset.remoteId!, @@ -202,7 +194,7 @@ class DownloadService { _buildDownloadTask( asset.remoteId!, asset.fileName, - group: asset.isImage ? downloadGroupImage : downloadGroupVideo, + group: asset.isImage ? kDownloadGroupImage : kDownloadGroupVideo, ), ]; } diff --git a/mobile/lib/services/entity.service.dart b/mobile/lib/services/entity.service.dart index 5837a6853c..468cc8f684 100644 --- a/mobile/lib/services/entity.service.dart +++ b/mobile/lib/services/entity.service.dart @@ -2,14 +2,13 @@ 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/interfaces/asset.interface.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; class EntityService { - final IAssetRepository _assetRepository; + final AssetRepository _assetRepository; final IsarUserRepository _isarUserRepository; - EntityService( + const EntityService( this._assetRepository, this._isarUserRepository, ); @@ -21,25 +20,21 @@ class EntityService { final user = await _isarUserRepository.getByUserId(ownerId); album.owner.value = user == null ? null : User.fromDto(user); } - final thumbnailAssetId = - album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId; + final thumbnailAssetId = album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId; if (thumbnailAssetId != null) { // set thumbnail with asset from database - album.thumbnail.value = - await _assetRepository.getByRemoteId(thumbnailAssetId); + 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()); + 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!)); + final assets = await _assetRepository.getAllByRemoteId(album.remoteAssets.map((asset) => asset.remoteId!)); album.assets.clear(); album.assets.addAll(assets); } diff --git a/mobile/lib/services/etag.service.dart b/mobile/lib/services/etag.service.dart index 6dd8a76bb3..00eb83fcea 100644 --- a/mobile/lib/services/etag.service.dart +++ b/mobile/lib/services/etag.service.dart @@ -1,14 +1,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/interfaces/etag.interface.dart'; import 'package:immich_mobile/repositories/etag.repository.dart'; -final etagServiceProvider = - Provider((ref) => ETagService(ref.watch(etagRepositoryProvider))); +final etagServiceProvider = Provider((ref) => ETagService(ref.watch(etagRepositoryProvider))); class ETagService { - final IETagRepository _eTagRepository; + final ETagRepository _eTagRepository; - ETagService(this._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 index 973f04303e..57f793b21e 100644 --- a/mobile/lib/services/exif.service.dart +++ b/mobile/lib/services/exif.service.dart @@ -1,12 +1,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/interfaces/exif.interface.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))); +final exifServiceProvider = Provider((ref) => ExifService(ref.watch(exifRepositoryProvider))); class ExifService { - final IExifInfoRepository _exifInfoRepository; + final IsarExifRepository _exifInfoRepository; const ExifService(this._exifInfoRepository); diff --git a/mobile/lib/services/folder.service.dart b/mobile/lib/services/folder.service.dart index 5b97b475b2..3f4936bb61 100644 --- a/mobile/lib/services/folder.service.dart +++ b/mobile/lib/services/folder.service.dart @@ -30,15 +30,13 @@ class FolderService { fullPath = '/$fullPath'; } - List segments = fullPath.split('/') - ..removeWhere((s) => s.isEmpty); + List segments = fullPath.split('/')..removeWhere((s) => s.isEmpty); String currentPath = ''; for (int i = 0; i < segments.length; i++) { String parentPath = currentPath.isEmpty ? '_root_' : currentPath; - currentPath = - i == 0 ? '/${segments[i]}' : '$currentPath/${segments[i]}'; + currentPath = i == 0 ? '/${segments[i]}' : '$currentPath/${segments[i]}'; if (!folderMap.containsKey(parentPath)) { folderMap[parentPath] = []; @@ -54,26 +52,20 @@ class FolderService { ); // Sort folders based on order parameter folderMap[parentPath]!.sort( - (a, b) => order == SortOrder.desc - ? b.name.compareTo(a.name) - : a.name.compareTo(b.name), + (a, b) => order == SortOrder.desc ? b.name.compareTo(a.name) : a.name.compareTo(b.name), ); } } } void attachSubfolders(RecursiveFolder folder) { - String fullPath = folder.path.isEmpty - ? '/${folder.name}' - : '${folder.path}/${folder.name}'; + String fullPath = folder.path.isEmpty ? '/${folder.name}' : '${folder.path}/${folder.name}'; if (folderMap.containsKey(fullPath)) { folder.subfolders.addAll(folderMap[fullPath]!); // Sort subfolders based on order parameter folder.subfolders.sort( - (a, b) => order == SortOrder.desc - ? b.name.compareTo(a.name) - : a.name.compareTo(b.name), + (a, b) => order == SortOrder.desc ? b.name.compareTo(a.name) : a.name.compareTo(b.name), ); for (var subfolder in folder.subfolders) { attachSubfolders(subfolder); @@ -84,9 +76,7 @@ class FolderService { List rootSubfolders = folderMap['_root_'] ?? []; // Sort root subfolders based on order parameter rootSubfolders.sort( - (a, b) => order == SortOrder.desc - ? b.name.compareTo(a.name) - : a.name.compareTo(b.name), + (a, b) => order == SortOrder.desc ? b.name.compareTo(a.name) : a.name.compareTo(b.name), ); for (var folder in rootSubfolders) { @@ -105,8 +95,7 @@ class FolderService { ) async { try { if (folder is RecursiveFolder) { - String fullPath = - folder.path.isEmpty ? folder.name : '${folder.path}/${folder.name}'; + String fullPath = folder.path.isEmpty ? folder.name : '${folder.path}/${folder.name}'; fullPath = fullPath[0] == '/' ? fullPath.substring(1) : fullPath; var result = await _folderApiRepository.getAssetsForPath(fullPath); diff --git a/mobile/lib/services/gcast.service.dart b/mobile/lib/services/gcast.service.dart index 60c94c712c..de9b8bbcb2 100644 --- a/mobile/lib/services/gcast.service.dart +++ b/mobile/lib/services/gcast.service.dart @@ -2,8 +2,7 @@ import 'dart:async'; import 'package:cast/session.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/interfaces/cast_destination_service.interface.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/models/sessions/session_create_response.model.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; @@ -21,7 +20,7 @@ final gCastServiceProvider = Provider( ), ); -class GCastService implements ICastDestinationService { +class GCastService { final GCastRepository _gCastRepository; final SessionsAPIRepository _sessionsApiService; final AssetApiRepository _assetApiRepository; @@ -32,15 +31,14 @@ class GCastService implements ICastDestinationService { int? _sessionId; Timer? _mediaStatusPollingTimer; - @override void Function(bool)? onConnectionState; - @override + void Function(Duration)? onCurrentTime; - @override + void Function(Duration)? onDuration; - @override + void Function(String)? onReceiverName; - @override + void Function(CastState)? onCastState; GCastService( @@ -73,8 +71,7 @@ class GCastService implements ICastDestinationService { } void _handleMediaStatus(Map message) { - final statusList = - (message['status'] as List).whereType>().toList(); + final statusList = (message['status'] as List).whereType>().toList(); if (statusList.isEmpty) { return; @@ -114,31 +111,26 @@ class GCastService implements ICastDestinationService { } if (status["currentTime"] != null) { - final currentTime = - Duration(milliseconds: (status["currentTime"] * 1000 ?? 0).toInt()); + final currentTime = Duration(milliseconds: (status["currentTime"] * 1000 ?? 0).toInt()); onCurrentTime?.call(currentTime); } } - @override Future connect(dynamic device) async { await _gCastRepository.connect(device); onReceiverName?.call(device.extras["fn"] ?? "Google Cast"); } - @override CastDestinationType getType() { return CastDestinationType.googleCast; } - @override Future initialize() async { // there is nothing blocking us from using Google Cast that we can check for return true; } - @override Future disconnect() async { onReceiverName?.call(""); currentAssetId = null; @@ -156,19 +148,15 @@ class GCastService implements ICastDestinationService { // we want to make sure we have at least 10 seconds remaining in the session // this is to account for network latency and other delays when sending the request - final bufferedExpiration = - tokenExpiration.subtract(const Duration(seconds: 10)); + final bufferedExpiration = tokenExpiration.subtract(const Duration(seconds: 10)); return bufferedExpiration.isAfter(DateTime.now()); } - @override - void loadMedia(Asset asset, bool reload) async { + void loadMedia(RemoteAsset asset, bool reload) async { if (!isConnected) { return; - } else if (asset.remoteId == null) { - return; - } else if (asset.remoteId == currentAssetId && !reload) { + } else if (asset.id == currentAssetId && !reload) { return; } @@ -183,19 +171,17 @@ class GCastService implements ICastDestinationService { final unauthenticatedUrl = asset.isVideo ? getPlaybackUrlForRemoteId( - asset.remoteId!, + asset.id, ) : getThumbnailUrlForRemoteId( - asset.remoteId!, + asset.id, type: AssetMediaSize.fullsize, ); - final authenticatedURL = - "$unauthenticatedUrl&sessionKey=${sessionKey?.token}"; + final authenticatedURL = "$unauthenticatedUrl&sessionKey=${sessionKey?.token}"; // get image mime type - final mimeType = - await _assetApiRepository.getAssetMIMEType(asset.remoteId!); + final mimeType = await _assetApiRepository.getAssetMIMEType(asset.id); if (mimeType == null) { return; @@ -212,7 +198,7 @@ class GCastService implements ICastDestinationService { "autoplay": true, }); - currentAssetId = asset.remoteId; + currentAssetId = asset.id; // we need to poll for media status since the cast device does not // send a message when the media is loaded for whatever reason @@ -220,8 +206,7 @@ class GCastService implements ICastDestinationService { _mediaStatusPollingTimer?.cancel(); if (asset.isVideo) { - _mediaStatusPollingTimer = - Timer.periodic(const Duration(milliseconds: 500), (timer) { + _mediaStatusPollingTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) { if (isConnected) { _gCastRepository.sendMessage(CastSession.kNamespaceMedia, { "type": "GET_STATUS", @@ -234,7 +219,6 @@ class GCastService implements ICastDestinationService { } } - @override void play() { _gCastRepository.sendMessage(CastSession.kNamespaceMedia, { "type": "PLAY", @@ -242,7 +226,6 @@ class GCastService implements ICastDestinationService { }); } - @override void pause() { _gCastRepository.sendMessage(CastSession.kNamespaceMedia, { "type": "PAUSE", @@ -250,7 +233,6 @@ class GCastService implements ICastDestinationService { }); } - @override void seekTo(Duration position) { _gCastRepository.sendMessage(CastSession.kNamespaceMedia, { "type": "SEEK", @@ -259,7 +241,6 @@ class GCastService implements ICastDestinationService { }); } - @override void stop() { _gCastRepository.sendMessage(CastSession.kNamespaceMedia, { "type": "STOP", @@ -273,17 +254,12 @@ class GCastService implements ICastDestinationService { // 0x01 is display capability bitmask bool isDisplay(int ca) => (ca & 0x01) != 0; - @override Future> getDevices() async { final dests = await _gCastRepository.listDestinations(); return dests .map( - (device) => ( - device.extras["fn"] ?? "Google Cast", - CastDestinationType.googleCast, - device - ), + (device) => (device.extras["fn"] ?? "Google Cast", CastDestinationType.googleCast, device), ) .where((device) { final caString = device.$3.extras["ca"]; diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart index f8a09471e4..f0554bf00b 100644 --- a/mobile/lib/services/hash.service.dart +++ b/mobile/lib/services/hash.service.dart @@ -4,23 +4,23 @@ 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/interfaces/device_asset.interface.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 IDeviceAssetRepository deviceAssetRepository, + required IsarDeviceAssetRepository deviceAssetRepository, required BackgroundService backgroundService, this.batchSizeLimit = kBatchHashSizeLimit, this.batchFileLimit = kBatchHashFileLimit, }) : _deviceAssetRepository = deviceAssetRepository, _backgroundService = backgroundService; - final IDeviceAssetRepository _deviceAssetRepository; + final IsarDeviceAssetRepository _deviceAssetRepository; final BackgroundService _backgroundService; final int batchSizeLimit; final int batchFileLimit; diff --git a/mobile/lib/services/local_auth.service.dart b/mobile/lib/services/local_auth.service.dart index c4abf0dbb2..12da5f256b 100644 --- a/mobile/lib/services/local_auth.service.dart +++ b/mobile/lib/services/local_auth.service.dart @@ -11,7 +11,7 @@ final localAuthServiceProvider = Provider( class LocalAuthService { final BiometricRepository _biometricRepository; - LocalAuthService(this._biometricRepository); + const LocalAuthService(this._biometricRepository); Future getStatus() { return _biometricRepository.getStatus(); diff --git a/mobile/lib/utils/local_files_manager.dart b/mobile/lib/services/local_files_manager.service.dart similarity index 60% rename from mobile/lib/utils/local_files_manager.dart rename to mobile/lib/services/local_files_manager.service.dart index a4cf41a6e6..ae935a131c 100644 --- a/mobile/lib/utils/local_files_manager.dart +++ b/mobile/lib/services/local_files_manager.service.dart @@ -1,21 +1,26 @@ import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; -abstract final class LocalFilesManager { +final localFileManagerServiceProvider = Provider( + (ref) => const LocalFilesManagerService(), +); + +class LocalFilesManagerService { + const LocalFilesManagerService(); static final Logger _logger = Logger('LocalFilesManager'); static const MethodChannel _channel = MethodChannel('file_trash'); - static Future moveToTrash(List mediaUrls) async { + Future moveToTrash(List mediaUrls) async { try { - return await _channel - .invokeMethod('moveToTrash', {'mediaUrls': mediaUrls}); + return await _channel.invokeMethod('moveToTrash', {'mediaUrls': mediaUrls}); } catch (e, s) { _logger.warning('Error moving file to trash', e, s); return false; } } - static Future restoreFromTrash(String fileName, int type) async { + Future restoreFromTrash(String fileName, int type) async { try { return await _channel.invokeMethod( 'restoreFromTrash', @@ -27,7 +32,7 @@ abstract final class LocalFilesManager { } } - static Future requestManageMediaPermission() async { + Future requestManageMediaPermission() async { try { return await _channel.invokeMethod('requestManageMediaPermission'); } catch (e, s) { diff --git a/mobile/lib/services/local_notification.service.dart b/mobile/lib/services/local_notification.service.dart index b47ee280b8..d67cc23616 100644 --- a/mobile/lib/services/local_notification.service.dart +++ b/mobile/lib/services/local_notification.service.dart @@ -13,8 +13,7 @@ final localNotificationService = Provider( ); class LocalNotificationService { - final FlutterLocalNotificationsPlugin _localNotificationPlugin = - FlutterLocalNotificationsPlugin(); + final FlutterLocalNotificationsPlugin _localNotificationPlugin = FlutterLocalNotificationsPlugin(); final PermissionStatus _permissionStatus; final Ref ref; @@ -29,17 +28,14 @@ class LocalNotificationService { static const cancelUploadActionID = 'cancel_upload'; Future setup() async { - const androidSetting = - AndroidInitializationSettings('@drawable/notification_icon'); + const androidSetting = AndroidInitializationSettings('@drawable/notification_icon'); const iosSetting = DarwinInitializationSettings(); - const initSettings = - InitializationSettings(android: androidSetting, iOS: iosSetting); + const initSettings = InitializationSettings(android: androidSetting, iOS: iosSetting); await _localNotificationPlugin.initialize( initSettings, - onDidReceiveNotificationResponse: - _onDidReceiveForegroundNotificationResponse, + onDidReceiveNotificationResponse: _onDidReceiveForegroundNotificationResponse, ); } diff --git a/mobile/lib/services/memory.service.dart b/mobile/lib/services/memory.service.dart index ab0e685778..ca877e7c78 100644 --- a/mobile/lib/services/memory.service.dart +++ b/mobile/lib/services/memory.service.dart @@ -1,6 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/models/memories/memory.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; @@ -18,7 +17,7 @@ class MemoryService { final log = Logger("MemoryService"); final ApiService _apiService; - final IAssetRepository _assetRepository; + final AssetRepository _assetRepository; MemoryService(this._apiService, this._assetRepository); @@ -36,8 +35,7 @@ class MemoryService { List memories = []; for (final memory in data) { - final dbAssets = await _assetRepository - .getAllByRemoteId(memory.assets.map((e) => e.id)); + 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( @@ -60,4 +58,33 @@ class MemoryService { 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/network.service.dart b/mobile/lib/services/network.service.dart index ac91a56f2c..de55da8d7c 100644 --- a/mobile/lib/services/network.service.dart +++ b/mobile/lib/services/network.service.dart @@ -13,7 +13,7 @@ class NetworkService { final NetworkRepository _repository; final IPermissionRepository _permissionRepository; - NetworkService(this._repository, this._permissionRepository); + const NetworkService(this._repository, this._permissionRepository); Future getLocationWhenInUserPermission() { return _permissionRepository.hasLocationWhenInUsePermission(); diff --git a/mobile/lib/services/partner.service.dart b/mobile/lib/services/partner.service.dart index ec210fd587..7b4f8a09b0 100644 --- a/mobile/lib/services/partner.service.dart +++ b/mobile/lib/services/partner.service.dart @@ -45,8 +45,7 @@ class PartnerService { Future removePartner(UserDto partner) async { try { await _partnerApiRepository.delete(partner.id); - await _isarUserRepository - .update(partner.copyWith(isPartnerSharedBy: false)); + await _isarUserRepository.update(partner.copyWith(isPartnerSharedBy: false)); } catch (e) { _log.warning("Failed to remove partner ${partner.id}", e); return false; @@ -57,8 +56,7 @@ class PartnerService { Future addPartner(UserDto partner) async { try { await _partnerApiRepository.create(partner.id); - await _isarUserRepository - .update(partner.copyWith(isPartnerSharedBy: true)); + await _isarUserRepository.update(partner.copyWith(isPartnerSharedBy: true)); return true; } catch (e) { _log.warning("Failed to add partner ${partner.id}", e); @@ -75,8 +73,7 @@ class PartnerService { partner.id, inTimeline: inTimeline, ); - await _isarUserRepository - .update(partner.copyWith(inTimeline: dto.inTimeline)); + await _isarUserRepository.update(partner.copyWith(inTimeline: dto.inTimeline)); return true; } catch (e) { _log.warning("Failed to update partner ${partner.id}", e); diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index c329187241..a2f7203d37 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -1,8 +1,6 @@ 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/interfaces/asset.interface.dart'; -import 'package:immich_mobile/interfaces/asset_api.interface.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'; @@ -21,8 +19,8 @@ PersonService personService(Ref ref) => PersonService( class PersonService { final Logger _log = Logger("PersonService"); final PersonApiRepository _personApiRepository; - final IAssetApiRepository _assetApiRepository; - final IAssetRepository _assetRepository; + final AssetApiRepository _assetApiRepository; + final AssetRepository _assetRepository; PersonService( this._personApiRepository, @@ -30,7 +28,7 @@ class PersonService { this._assetRepository, ); - Future> getAllPeople() async { + Future> getAllPeople() async { try { return await _personApiRepository.getAll(); } catch (error, stack) { @@ -42,15 +40,14 @@ class PersonService { Future> getPersonAssets(String id) async { try { final assets = await _assetApiRepository.search(personIds: [id]); - return await _assetRepository - .getAllByRemoteId(assets.map((a) => a.remoteId!)); + 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 { + Future updateName(String id, String name) async { try { return await _personApiRepository.update(id, name: name); } catch (error, stack) { diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index bcf67889c0..2aa39fcf80 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:immich_mobile/interfaces/asset.interface.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/entities/asset.entity.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:logging/logging.dart'; @@ -15,15 +15,21 @@ final searchServiceProvider = Provider( (ref) => SearchService( ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider), + ref.watch(searchApiRepositoryProvider), ), ); class SearchService { final ApiService _apiService; - final IAssetRepository _assetRepository; + final AssetRepository _assetRepository; + final SearchApiRepository _searchApiRepository; final _log = Logger("SearchService"); - SearchService(this._apiService, this._assetRepository); + SearchService( + this._apiService, + this._assetRepository, + this._searchApiRepository, + ); Future?> getSearchSuggestions( SearchSuggestionType type, { @@ -33,7 +39,7 @@ class SearchService { String? model, }) async { try { - return await _apiService.searchApi.getSearchSuggestions( + return await _searchApiRepository.getSearchSuggestions( type, country: country, state: state, @@ -48,76 +54,14 @@ class SearchService { Future search(SearchFilter filter, int page) async { try { - SearchResponseDto? response; - AssetTypeEnum? type; - if (filter.mediaType == AssetType.image) { - type = AssetTypeEnum.IMAGE; - } else if (filter.mediaType == AssetType.video) { - type = AssetTypeEnum.VIDEO; - } - - if (filter.context != null && filter.context!.isNotEmpty) { - response = await _apiService.searchApi.searchSmart( - SmartSearchDto( - query: filter.context!, - language: filter.language, - country: filter.location.country, - state: filter.location.state, - city: filter.location.city, - make: filter.camera.make, - model: filter.camera.model, - takenAfter: filter.date.takenAfter, - takenBefore: filter.date.takenBefore, - visibility: filter.display.isArchive - ? AssetVisibility.archive - : AssetVisibility.timeline, - isFavorite: filter.display.isFavorite ? true : null, - isNotInAlbum: filter.display.isNotInAlbum ? true : null, - personIds: filter.people.map((e) => e.id).toList(), - type: type, - page: page, - size: 1000, - ), - ); - } else { - response = await _apiService.searchApi.searchAssets( - MetadataSearchDto( - originalFileName: - filter.filename != null && filter.filename!.isNotEmpty - ? filter.filename - : null, - country: filter.location.country, - description: - filter.description != null && filter.description!.isNotEmpty - ? filter.description - : null, - state: filter.location.state, - city: filter.location.city, - make: filter.camera.make, - model: filter.camera.model, - takenAfter: filter.date.takenAfter, - takenBefore: filter.date.takenBefore, - visibility: filter.display.isArchive - ? AssetVisibility.archive - : AssetVisibility.timeline, - isFavorite: filter.display.isFavorite ? true : null, - isNotInAlbum: filter.display.isNotInAlbum ? true : null, - personIds: filter.people.map((e) => e.id).toList(), - type: type, - page: page, - size: 1000, - ), - ); - } + 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), - ), + assets: await _assetRepository.getAllByRemoteId(response.assets.items.map((e) => e.id)), nextPage: response.assets.nextPage?.toInt(), ); } catch (error, stackTrace) { diff --git a/mobile/lib/services/secure_storage.service.dart b/mobile/lib/services/secure_storage.service.dart index e9b67257b8..38e6deb0d4 100644 --- a/mobile/lib/services/secure_storage.service.dart +++ b/mobile/lib/services/secure_storage.service.dart @@ -10,7 +10,7 @@ final secureStorageServiceProvider = Provider( class SecureStorageService { final SecureStorageRepository _secureStorageRepository; - SecureStorageService(this._secureStorageRepository); + const SecureStorageService(this._secureStorageRepository); Future write(String key, String value) async { await _secureStorageRepository.write(key, value); diff --git a/mobile/lib/services/server_info.service.dart b/mobile/lib/services/server_info.service.dart index e2b7db2fce..75ce68a73c 100644 --- a/mobile/lib/services/server_info.service.dart +++ b/mobile/lib/services/server_info.service.dart @@ -16,7 +16,7 @@ final serverInfoServiceProvider = Provider( class ServerInfoService { final ApiService _apiService; - ServerInfoService(this._apiService); + const ServerInfoService(this._apiService); Future getDiskInfo() async { try { diff --git a/mobile/lib/services/share.service.dart b/mobile/lib/services/share.service.dart index 77afa10fb6..fec8b76af0 100644 --- a/mobile/lib/services/share.service.dart +++ b/mobile/lib/services/share.service.dart @@ -10,8 +10,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'api.service.dart'; -final shareServiceProvider = - Provider((ref) => ShareService(ref.watch(apiServiceProvider))); +final shareServiceProvider = Provider((ref) => ShareService(ref.watch(apiServiceProvider))); class ShareService { final ApiService _apiService; @@ -37,8 +36,7 @@ class ShareService { final tempDir = await getTemporaryDirectory(); final fileName = asset.fileName; final tempFile = await File('${tempDir.path}/$fileName').create(); - final res = await _apiService.assetsApi - .downloadAssetWithHttpInfo(asset.remoteId!); + final res = await _apiService.assetsApi.downloadAssetWithHttpInfo(asset.remoteId!); if (res.statusCode != 200) { _log.severe( diff --git a/mobile/lib/services/shared_link.service.dart b/mobile/lib/services/shared_link.service.dart index a2b5ed9062..44923b39d7 100644 --- a/mobile/lib/services/shared_link.service.dart +++ b/mobile/lib/services/shared_link.service.dart @@ -18,9 +18,7 @@ class SharedLinkService { Future>> getAllSharedLinks() async { try { final list = await _apiService.sharedLinksApi.getAllSharedLinks(); - return list != null - ? AsyncData(list.map(SharedLink.fromDto).toList()) - : const AsyncData([]); + return list != null ? AsyncData(list.map(SharedLink.fromDto).toList()) : const AsyncData([]); } catch (e, stack) { _log.severe("Failed to fetch shared links", e, stack); return AsyncError(e, stack); @@ -46,8 +44,7 @@ class SharedLinkService { DateTime? expiresAt, }) async { try { - final type = - albumId != null ? SharedLinkType.ALBUM : SharedLinkType.INDIVIDUAL; + final type = albumId != null ? SharedLinkType.ALBUM : SharedLinkType.INDIVIDUAL; SharedLinkCreateDto? dto; if (type == SharedLinkType.ALBUM) { dto = SharedLinkCreateDto( @@ -74,8 +71,7 @@ class SharedLinkService { } if (dto != null) { - final responseDto = - await _apiService.sharedLinksApi.createSharedLink(dto); + final responseDto = await _apiService.sharedLinksApi.createSharedLink(dto); if (responseDto != null) { return SharedLink.fromDto(responseDto); } diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart index 1ca56ff279..d46c0c0b99 100644 --- a/mobile/lib/services/stack.service.dart +++ b/mobile/lib/services/stack.service.dart @@ -1,17 +1,16 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/interfaces/asset.interface.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'; class StackService { - StackService(this._api, this._assetRepository); + const StackService(this._api, this._assetRepository); final ApiService _api; - final IAssetRepository _assetRepository; + final AssetRepository _assetRepository; Future getStack(String stackId) async { try { @@ -61,8 +60,7 @@ class StackService { removeAssets.add(asset); } - await _assetRepository - .transaction(() => _assetRepository.updateAll(removeAssets)); + await _assetRepository.transaction(() => _assetRepository.updateAll(removeAssets)); } catch (error) { debugPrint("Error while deleting stack: $error"); } diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index 5013411599..723dc9c34b 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -4,19 +4,15 @@ 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/interfaces/exif.interface.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/interfaces/album.interface.dart'; -import 'package:immich_mobile/interfaces/asset.interface.dart'; -import 'package:immich_mobile/interfaces/etag.interface.dart'; -import 'package:immich_mobile/interfaces/local_files_manager.interface.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'; @@ -62,19 +58,19 @@ class SyncService { final EntityService _entityService; final AlbumMediaRepository _albumMediaRepository; final AlbumApiRepository _albumApiRepository; - final IAlbumRepository _albumRepository; - final IAssetRepository _assetRepository; - final IExifInfoRepository _exifInfoRepository; + final AlbumRepository _albumRepository; + final AssetRepository _assetRepository; + final IsarExifRepository _exifInfoRepository; final IsarUserRepository _isarUserRepository; final UserService _userService; final PartnerRepository _partnerRepository; - final IETagRepository _eTagRepository; + final ETagRepository _eTagRepository; final PartnerApiRepository _partnerApiRepository; final UserApiRepository _userApiRepository; final AsyncMutex _lock = AsyncMutex(); final Logger _log = Logger('SyncService'); final AppSettingsService _appSettingsService; - final ILocalFilesManager _localFilesManager; + final LocalFilesManagerRepository _localFilesManager; SyncService( this._hashService, @@ -98,8 +94,7 @@ class SyncService { /// Syncs users from the server to the local database /// Returns `true`if there were any changes - Future syncUsersFromServer(List users) => - _lock.run(() => _syncUsersFromServer(users)); + 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 @@ -109,8 +104,7 @@ class SyncService { List users, DateTime since, ) getChangedAssets, - required FutureOr?> Function(UserDto user, DateTime until) - loadAssets, + required FutureOr?> Function(UserDto user, DateTime until) loadAssets, }) => _lock.run( () async => @@ -143,18 +137,13 @@ class SyncService { } deleteCandidates.sort(Asset.compareById); existing.sort(Asset.compareById); - return _diffAssets(existing, deleteCandidates, compare: Asset.compareById) - .$3 - .map((e) => e.id) - .toList(); + 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 syncNewAssetToDb(Asset newAsset) => _lock.run(() => _syncNewAssetToDb(newAsset)); - Future removeAllLocalAlbumsAndAssets() => - _lock.run(_removeAllLocalAlbumsAndAssets); + Future removeAllLocalAlbumsAndAssets() => _lock.run(_removeAllLocalAlbumsAndAssets); // private methods: @@ -193,8 +182,7 @@ class SyncService { /// 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); + 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 @@ -218,8 +206,7 @@ class SyncService { ) getChangedAssets, ) async { final currentUser = _userService.getMyUser(); - final DateTime? since = - (await _eTagRepository.get(currentUser.id))?.time?.toUtc(); + 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); @@ -248,13 +235,10 @@ class SyncService { Future _moveToTrashMatchedAssets(Iterable idsToDelete) async { final List localAssets = await _assetRepository.getAllLocal(); - final List matchedAssets = localAssets - .where((asset) => idsToDelete.contains(asset.remoteId)) - .toList(); + 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)), + matchedAssets.map((asset) => asset.local?.getMediaUrl() ?? Future.value(null)), ); await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); @@ -375,10 +359,8 @@ class SyncService { final bool changes = await diffSortedLists( remoteAlbums, dbAlbums, - compare: (remoteAlbum, dbAlbum) => - remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!), - both: (remoteAlbum, dbAlbum) => - _syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing), + 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), ); @@ -425,11 +407,9 @@ class SyncService { ); // update shared users - final List sharedUsers = - album.sharedUsers.map((u) => u.toDto()).toList(growable: false); + 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 users = dto.remoteUsers.map((u) => u.toDto()).toList()..sort((a, b) => a.id.compareTo(b.id)); final List userIdsToAdd = []; final List usersToUnlink = []; diffSortedListsSync( @@ -460,10 +440,8 @@ class SyncService { album.sortOrder = dto.sortOrder; final remoteThumbnailAssetId = dto.remoteThumbnailAssetId; - if (remoteThumbnailAssetId != null && - album.thumbnail.value?.remoteId != remoteThumbnailAssetId) { - album.thumbnail.value = - await _assetRepository.getByRemoteId(remoteThumbnailAssetId); + if (remoteThumbnailAssetId != null && album.thumbnail.value?.remoteId != remoteThumbnailAssetId) { + album.thumbnail.value = await _assetRepository.getByRemoteId(remoteThumbnailAssetId); } // write & commit all changes to DB @@ -484,8 +462,7 @@ class SyncService { if (album.shared || dto.shared) { final userId = (_userService.getMyUser()).id; - final foreign = - await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); + 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 @@ -509,16 +486,14 @@ class SyncService { 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()); + 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} != " + _log.warning("Failed to add album from server: assetCount ${album.remoteAssetCount} != " "asset array length ${album.remoteAssets.length} for album ${album.name}"); } } @@ -538,8 +513,7 @@ class SyncService { } 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); + final orphanedAssets = await _assetRepository.getByAlbum(album, notOwnedBy: userIds); deleteCandidates.addAll(orphanedAssets); } try { @@ -557,8 +531,7 @@ class SyncService { Set? excludedAssets, ]) async { onDevice.sort((a, b) => a.localId!.compareTo(b.localId!)); - final inDb = - await _albumRepository.getAll(remote: false, sortBy: AlbumSort.localId); + final inDb = await _albumRepository.getAll(remote: false, sortBy: AlbumSort.localId); final List deleteCandidates = []; final List existing = []; final bool anyChanges = await diffSortedLists( @@ -578,8 +551,7 @@ class SyncService { _log.fine( "Syncing all local albums almost done. Collected ${deleteCandidates.length} asset candidates to delete", ); - final (toDelete, toUpdate) = - _handleAssetRemoval(deleteCandidates, existing, remote: false); + final (toDelete, toUpdate) = _handleAssetRemoval(deleteCandidates, existing, remote: false); _log.fine( "${toDelete.length} assets to delete, ${toUpdate.length} to update", ); @@ -614,9 +586,7 @@ class SyncService { return false; } _log.info("Local album ${deviceAlbum.name} has changed. Syncing..."); - if (!forceRefresh && - excludedAssets == null && - await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) { + if (!forceRefresh && excludedAssets == null && await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) { _log.info("Fast synced local album ${deviceAlbum.name} to DB"); return true; } @@ -628,8 +598,7 @@ class SyncService { ); assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); - final int assetCountOnDevice = - await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); + final int assetCountOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); final List onDevice = await _getHashedAssets( deviceAlbum, excludedAssets: excludedAssets, @@ -647,9 +616,7 @@ class SyncService { _log.info( "Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync.", ); - if (assetCountOnDevice != - (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount)) - ?.assetCount) { + if (assetCountOnDevice != (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount) { await _eTagRepository.upsertAll([ ETag( id: deviceAlbum.eTagKeyAssetCount, @@ -671,8 +638,7 @@ class SyncService { dbAlbum.name = deviceAlbum.name; dbAlbum.description = deviceAlbum.description; dbAlbum.modifiedAt = deviceAlbum.modifiedAt; - if (dbAlbum.thumbnail.value != null && - toDelete.contains(dbAlbum.thumbnail.value)) { + if (dbAlbum.thumbnail.value != null && toDelete.contains(dbAlbum.thumbnail.value)) { dbAlbum.thumbnail.value = null; } try { @@ -706,12 +672,8 @@ class SyncService { ); return false; } - final int totalOnDevice = - await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); - final int lastKnownTotal = - (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount)) - ?.assetCount ?? - 0; + 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.", @@ -780,8 +742,7 @@ class SyncService { album.thumbnail.value = thumb; try { await _albumRepository.create(album); - final int assetCount = - await _albumMediaRepository.getAssetCount(album.localId!); + final int assetCount = await _albumMediaRepository.getAssetCount(album.localId!); await _eTagRepository.upsertAll([ ETag(id: album.eTagKeyAssetCount, assetCount: assetCount), ]); @@ -917,9 +878,8 @@ class SyncService { modifiedFrom: modifiedFrom, modifiedUntil: modifiedUntil, ); - final filtered = excludedAssets == null - ? entities - : entities.where((e) => !excludedAssets.contains(e.localId!)).toList(); + final filtered = + excludedAssets == null ? entities : entities.where((e) => !excludedAssets.contains(e.localId!)).toList(); return _hashService.hashAssets(filtered); } @@ -946,15 +906,13 @@ class SyncService { deviceAlbum.description != dbAlbum.description || !deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) != - (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount)) - ?.assetCount; + (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount; } Future _removeAllLocalAlbumsAndAssets() async { try { final assets = await _assetRepository.getAllLocal(); - final (toDelete, toUpdate) = - _handleAssetRemoval(assets, [], remote: false); + final (toDelete, toUpdate) = _handleAssetRemoval(assets, [], remote: false); await _assetRepository.transaction(() async { await _assetRepository.deleteByIds(toDelete); await _assetRepository.updateAll(toUpdate); @@ -975,10 +933,8 @@ class SyncService { _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); + 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"); diff --git a/mobile/lib/services/timeline.service.dart b/mobile/lib/services/timeline.service.dart index 47ad17fc25..1a4f9e2685 100644 --- a/mobile/lib/services/timeline.service.dart +++ b/mobile/lib/services/timeline.service.dart @@ -103,8 +103,7 @@ class TimelineService { } GroupAssetsBy _getGroupByOption() { - return GroupAssetsBy - .values[_appSettingsService.getSetting(AppSettingsEnum.groupAssetsBy)]; + return GroupAssetsBy.values[_appSettingsService.getSetting(AppSettingsEnum.groupAssetsBy)]; } Stream watchLockedTimelineProvider() async* { diff --git a/mobile/lib/services/trash.service.dart b/mobile/lib/services/trash.service.dart index 478ad65db0..6cd7dfc641 100644 --- a/mobile/lib/services/trash.service.dart +++ b/mobile/lib/services/trash.service.dart @@ -1,7 +1,6 @@ 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/interfaces/asset.interface.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'; @@ -18,7 +17,7 @@ final trashServiceProvider = Provider((ref) { class TrashService { final ApiService _apiService; - final IAssetRepository _assetRepository; + final AssetRepository _assetRepository; final UserService _userService; const TrashService( diff --git a/mobile/lib/services/upload.service.dart b/mobile/lib/services/upload.service.dart index 0734e57212..e8acf791a2 100644 --- a/mobile/lib/services/upload.service.dart +++ b/mobile/lib/services/upload.service.dart @@ -1,75 +1,329 @@ +import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/interfaces/upload.interface.dart'; +import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/utils/upload.dart'; -import 'package:path/path.dart'; -// import 'package:logging/logging.dart'; +import 'package:path/path.dart' as p; -final uploadServiceProvider = Provider( - (ref) => UploadService( +final uploadServiceProvider = Provider((ref) { + final service = UploadService( ref.watch(uploadRepositoryProvider), - ), -); + ref.watch(backupRepositoryProvider), + ref.watch(storageRepositoryProvider), + ref.watch(localAssetRepository), + ); + + ref.onDispose(service.dispose); + return service; +}); class UploadService { - final IUploadRepository _uploadRepository; - // final Logger _log = Logger("UploadService"); - void Function(TaskStatusUpdate)? onUploadStatus; - void Function(TaskProgressUpdate)? onTaskProgress; - UploadService( this._uploadRepository, + this._backupRepository, + this._storageRepository, + this._localAssetRepository, ) { _uploadRepository.onUploadStatus = _onUploadCallback; _uploadRepository.onTaskProgress = _onTaskProgressCallback; } + final UploadRepository _uploadRepository; + final DriftBackupRepository _backupRepository; + final StorageRepository _storageRepository; + final DriftLocalAssetRepository _localAssetRepository; + + final StreamController _taskStatusController = StreamController.broadcast(); + final StreamController _taskProgressController = StreamController.broadcast(); + + Stream get taskStatusStream => _taskStatusController.stream; + Stream get taskProgressStream => _taskProgressController.stream; + + bool shouldAbortQueuingTasks = false; + void _onTaskProgressCallback(TaskProgressUpdate update) { - onTaskProgress?.call(update); + if (!_taskProgressController.isClosed) { + _taskProgressController.add(update); + } } void _onUploadCallback(TaskStatusUpdate update) { - onUploadStatus?.call(update); + if (!_taskStatusController.isClosed) { + _taskStatusController.add(update); + } + _handleTaskStatusUpdate(update); } - Future cancelUpload(String id) { - return FileDownloader().cancelTaskWithId(id); + void dispose() { + _taskStatusController.close(); + _taskProgressController.close(); } - Future upload(File file) async { - final task = await _buildUploadTask( - hash(file.path).toString(), + void enqueueTasks(List tasks) { + _uploadRepository.enqueueBackgroundAll(tasks); + } + + Future> getActiveTasks(String group) { + return _uploadRepository.getActiveTasks(group); + } + + Future getBackupTotalCount() { + return _backupRepository.getTotalCount(); + } + + Future getBackupRemainderCount(String userId) { + return _backupRepository.getRemainderCount(userId); + } + + Future getBackupFinishedCount(String userId) { + return _backupRepository.getBackupCount(userId); + } + + Future manualBackup(List localAssets) async { + List tasks = []; + for (final asset in localAssets) { + final task = await _getUploadTask( + asset, + group: kManualUploadGroup, + priority: 1, // High priority after upload motion photo part + ); + if (task != null) { + tasks.add(task); + } + } + + if (tasks.isNotEmpty) { + enqueueTasks(tasks); + } + } + + /// Find backup candidates + /// Build the upload tasks + /// Enqueue the tasks + Future startBackup( + String userId, + void Function(EnqueueStatus status) onEnqueueTasks, + ) async { + shouldAbortQueuingTasks = false; + + final candidates = await _backupRepository.getCandidates(userId); + if (candidates.isEmpty) { + return; + } + + const batchSize = 100; + int count = 0; + for (int i = 0; i < candidates.length; i += batchSize) { + if (shouldAbortQueuingTasks) { + break; + } + + final batch = candidates.skip(i).take(batchSize).toList(); + + List tasks = []; + for (final asset in batch) { + final task = await _getUploadTask(asset); + if (task != null) { + tasks.add(task); + } + } + + if (tasks.isNotEmpty && !shouldAbortQueuingTasks) { + count += tasks.length; + enqueueTasks(tasks); + + onEnqueueTasks( + EnqueueStatus( + enqueueCount: count, + totalCount: candidates.length, + ), + ); + } + } + } + + /// Cancel all ongoing uploads and reset the upload queue + /// + /// Return the number of left over tasks in the queue + Future cancelBackup() async { + shouldAbortQueuingTasks = true; + + await _uploadRepository.reset(kBackupGroup); + await _uploadRepository.deleteDatabaseRecords(kBackupGroup); + + final activeTasks = await _uploadRepository.getActiveTasks(kBackupGroup); + return activeTasks.length; + } + + Future resumeBackup() { + return _uploadRepository.start(); + } + + void _handleTaskStatusUpdate(TaskStatusUpdate update) { + switch (update.status) { + case TaskStatus.complete: + _handleLivePhoto(update); + break; + + default: + break; + } + } + + Future _handleLivePhoto(TaskStatusUpdate update) async { + try { + if (update.task.metaData.isEmpty || update.task.metaData == '') { + return; + } + + final metadata = UploadTaskMetadata.fromJson(update.task.metaData); + if (!metadata.isLivePhotos) { + return; + } + + if (update.responseBody == null || update.responseBody!.isEmpty) { + return; + } + final response = jsonDecode(update.responseBody!); + + final localAsset = await _localAssetRepository.getById(metadata.localAssetId); + if (localAsset == null) { + return; + } + + final uploadTask = await _getLivePhotoUploadTask( + localAsset, + response['id'] as String, + ); + + if (uploadTask == null) { + return; + } + + enqueueTasks([uploadTask]); + } catch (error, stackTrace) { + debugPrint("Error handling live photo upload task: $error $stackTrace"); + } + } + + Future _getUploadTask( + LocalAsset asset, { + String group = kBackupGroup, + int? priority, + }) async { + final entity = await _storageRepository.getAssetEntityForAsset(asset); + if (entity == null) { + return null; + } + + File? file; + + /// iOS LivePhoto has two files: a photo and a video. + /// They are uploaded separately, with video file being upload first, then returned with the assetId + /// The assetId is then used as a metadata for the photo file upload task. + /// + /// We implement two separate upload groups for this, the normal one for the video file + /// and the higher priority group for the photo file because the video file is already uploaded. + /// + /// The cancel operation will only cancel the video group (normal group), the photo group will not + /// be touched, as the video file is already uploaded. + + if (entity.isLivePhoto) { + file = await _storageRepository.getMotionFileForAsset(asset); + } else { + file = await _storageRepository.getFileForAsset(asset.id); + } + + if (file == null) { + return null; + } + + final originalFileName = entity.isLivePhoto + ? p.setExtension( + asset.name, + p.extension(file.path), + ) + : asset.name; + + String metadata = UploadTaskMetadata( + localAssetId: asset.id, + isLivePhotos: entity.isLivePhoto, + livePhotoVideoId: '', + ).toJson(); + + return buildUploadTask( file, + originalFileName: originalFileName, + deviceAssetId: asset.id, + metadata: metadata, + group: group, + priority: priority, ); - - await _uploadRepository.upload(task); } - Future _buildUploadTask( - String id, + Future _getLivePhotoUploadTask( + LocalAsset asset, + String livePhotoVideoId, + ) async { + final entity = await _storageRepository.getAssetEntityForAsset(asset); + if (entity == null) { + return null; + } + + final file = await _storageRepository.getFileForAsset(asset.id); + if (file == null) { + return null; + } + + final fields = { + 'livePhotoVideoId': livePhotoVideoId, + }; + + return buildUploadTask( + file, + originalFileName: asset.name, + deviceAssetId: asset.id, + fields: fields, + group: kBackupLivePhotoGroup, + priority: 0, // Highest priority to get upload immediately + ); + } + + Future buildUploadTask( File file, { + required String group, Map? fields, + String? originalFileName, + String? deviceAssetId, + String? metadata, + int? priority, }) async { final serverEndpoint = Store.get(StoreKey.serverEndpoint); final url = Uri.parse('$serverEndpoint/assets').toString(); final headers = ApiService.getRequestHeaders(); final deviceId = Store.get(StoreKey.deviceId); - final (baseDirectory, directory, filename) = - await Task.split(filePath: file.path); + final (baseDirectory, directory, filename) = await Task.split(filePath: file.path); final stats = await file.stat(); final fileCreatedAt = stats.changed; final fileModifiedAt = stats.modified; - final fieldsMap = { - 'filename': filename, - 'deviceAssetId': id, + 'filename': originalFileName ?? filename, + 'deviceAssetId': deviceAssetId ?? '', 'deviceId': deviceId, 'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(), 'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(), @@ -79,7 +333,8 @@ class UploadService { }; return UploadTask( - taskId: id, + taskId: deviceAssetId, + displayName: originalFileName ?? filename, httpRequestMethod: 'POST', url: url, headers: headers, @@ -88,8 +343,72 @@ class UploadService { baseDirectory: baseDirectory, directory: directory, fileField: 'assetData', - group: uploadGroup, + metaData: metadata ?? '', + group: group, + priority: priority ?? 5, updates: Updates.statusAndProgress, + retries: 3, ); } } + +class UploadTaskMetadata { + final String localAssetId; + final bool isLivePhotos; + final String livePhotoVideoId; + + const UploadTaskMetadata({ + required this.localAssetId, + required this.isLivePhotos, + required this.livePhotoVideoId, + }); + + UploadTaskMetadata copyWith({ + String? localAssetId, + bool? isLivePhotos, + String? livePhotoVideoId, + }) { + return UploadTaskMetadata( + localAssetId: localAssetId ?? this.localAssetId, + isLivePhotos: isLivePhotos ?? this.isLivePhotos, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + ); + } + + Map toMap() { + return { + 'localAssetId': localAssetId, + 'isLivePhotos': isLivePhotos, + 'livePhotoVideoId': livePhotoVideoId, + }; + } + + factory UploadTaskMetadata.fromMap(Map map) { + return UploadTaskMetadata( + localAssetId: map['localAssetId'] as String, + isLivePhotos: map['isLivePhotos'] as bool, + livePhotoVideoId: map['livePhotoVideoId'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory UploadTaskMetadata.fromJson(String source) => + UploadTaskMetadata.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'UploadTaskMetadata(localAssetId: $localAssetId, isLivePhotos: $isLivePhotos, livePhotoVideoId: $livePhotoVideoId)'; + + @override + bool operator ==(covariant UploadTaskMetadata other) { + if (identical(this, other)) return true; + + return other.localAssetId == localAssetId && + other.isLivePhotos == isLivePhotos && + other.livePhotoVideoId == livePhotoVideoId; + } + + @override + int get hashCode => localAssetId.hashCode ^ isLivePhotos.hashCode ^ livePhotoVideoId.hashCode; +} diff --git a/mobile/lib/services/widget.service.dart b/mobile/lib/services/widget.service.dart index bb7b367c27..fb2022784f 100644 --- a/mobile/lib/services/widget.service.dart +++ b/mobile/lib/services/widget.service.dart @@ -1,6 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/interfaces/widget.interface.dart'; import 'package:immich_mobile/repositories/widget.repository.dart'; final widgetServiceProvider = Provider((ref) { @@ -10,9 +9,9 @@ final widgetServiceProvider = Provider((ref) { }); class WidgetService { - final IWidgetRepository _repository; + final WidgetRepository _repository; - WidgetService(this._repository); + const WidgetService(this._repository); Future writeCredentials(String serverURL, String sessionKey) async { await _repository.setAppGroupId(appShareGroupId); @@ -33,8 +32,8 @@ class WidgetService { } Future refreshWidgets() async { - for (final name in kWidgetNames) { - await _repository.refresh(name); + for (final (iOSName, androidName) in kWidgetNames) { + await _repository.refresh(iOSName, androidName); } } } diff --git a/mobile/lib/theme/dynamic_theme.dart b/mobile/lib/theme/dynamic_theme.dart index 39d6b6ee45..8ebf783469 100644 --- a/mobile/lib/theme/dynamic_theme.dart +++ b/mobile/lib/theme/dynamic_theme.dart @@ -4,7 +4,7 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:immich_mobile/theme/theme_data.dart'; abstract final class DynamicTheme { - DynamicTheme._(); + const DynamicTheme._(); static ImmichTheme? _theme; // Method to fetch dynamic system colors diff --git a/mobile/lib/theme/theme_data.dart b/mobile/lib/theme/theme_data.dart index a351b09093..32695ef26e 100644 --- a/mobile/lib/theme/theme_data.dart +++ b/mobile/lib/theme/theme_data.dart @@ -45,8 +45,7 @@ ThemeData getThemeData({ fontWeight: FontWeight.w600, fontSize: 18, ), - backgroundColor: - isDark ? colorScheme.surfaceContainer : colorScheme.surface, + backgroundColor: isDark ? colorScheme.surfaceContainer : colorScheme.surface, foregroundColor: colorScheme.primary, elevation: 0, scrolledUnderElevation: 0, @@ -100,8 +99,7 @@ ThemeData getThemeData({ ), ), navigationBarTheme: NavigationBarThemeData( - backgroundColor: - isDark ? colorScheme.surfaceContainer : colorScheme.surface, + backgroundColor: isDark ? colorScheme.surfaceContainer : colorScheme.surface, labelTextStyle: const WidgetStatePropertyAll( TextStyle( fontSize: 14, diff --git a/mobile/lib/utils/backup_progress.dart b/mobile/lib/utils/backup_progress.dart index 38cdeacdb2..3d2af2877e 100644 --- a/mobile/lib/utils/backup_progress.dart +++ b/mobile/lib/utils/backup_progress.dart @@ -50,8 +50,7 @@ String humanReadableBytesProgress(int bytes, int bytesTotal) { } class ThrottleProgressUpdate { - ThrottleProgressUpdate(this._fun, Duration interval) - : _interval = interval.inMicroseconds; + ThrottleProgressUpdate(this._fun, Duration interval) : _interval = interval.inMicroseconds; final void Function(String?, int, int) _fun; final int _interval; int _invokedAt = 0; diff --git a/mobile/lib/utils/cache/custom_image_cache.dart b/mobile/lib/utils/cache/custom_image_cache.dart index dcb8dacb0d..6465a68222 100644 --- a/mobile/lib/utils/cache/custom_image_cache.dart +++ b/mobile/lib/utils/cache/custom_image_cache.dart @@ -1,4 +1,6 @@ import 'package:flutter/painting.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/providers/image/immich_local_image_provider.dart'; import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart'; import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart'; @@ -37,10 +39,12 @@ final class CustomImageCache implements ImageCache { /// Gets the cache for the given key /// [_large] is used for [ImmichLocalImageProvider] and [ImmichRemoteImageProvider] /// [_small] is used for [ImmichLocalThumbnailProvider] and [ImmichRemoteThumbnailProvider] - ImageCache _cacheForKey(Object key) => - (key is ImmichLocalImageProvider || key is ImmichRemoteImageProvider) - ? _large - : _small; + ImageCache _cacheForKey(Object key) => (key is ImmichLocalImageProvider || + key is ImmichRemoteImageProvider || + key is LocalFullImageProvider || + key is RemoteFullImageProvider) + ? _large + : _small; @override bool containsKey(Object key) { @@ -56,15 +60,13 @@ final class CustomImageCache implements ImageCache { int get currentSizeBytes => _small.currentSizeBytes + _large.currentSizeBytes; @override - bool evict(Object key, {bool includeLive = true}) => - _cacheForKey(key).evict(key, includeLive: includeLive); + bool evict(Object key, {bool includeLive = true}) => _cacheForKey(key).evict(key, includeLive: includeLive); @override int get liveImageCount => _small.liveImageCount + _large.liveImageCount; @override - int get pendingImageCount => - _small.pendingImageCount + _large.pendingImageCount; + int get pendingImageCount => _small.pendingImageCount + _large.pendingImageCount; @override ImageStreamCompleter? putIfAbsent( @@ -75,6 +77,5 @@ final class CustomImageCache implements ImageCache { _cacheForKey(key).putIfAbsent(key, loader, onError: onError); @override - ImageCacheStatus statusForKey(Object key) => - _cacheForKey(key).statusForKey(key); + ImageCacheStatus statusForKey(Object key) => _cacheForKey(key).statusForKey(key); } diff --git a/mobile/lib/utils/color_filter_generator.dart b/mobile/lib/utils/color_filter_generator.dart index c155823264..d4217a9319 100644 --- a/mobile/lib/utils/color_filter_generator.dart +++ b/mobile/lib/utils/color_filter_generator.dart @@ -82,8 +82,7 @@ class _ColorFilterGenerator { ]; } - double x = - ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble(); + double x = ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble(); double lumR = 0.3086; double lumG = 0.6094; double lumB = 0.082; diff --git a/mobile/lib/utils/database.utils.dart b/mobile/lib/utils/database.utils.dart new file mode 100644 index 0000000000..446b92db19 --- /dev/null +++ b/mobile/lib/utils/database.utils.dart @@ -0,0 +1,31 @@ +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; + +extension LocalAlbumEntityDataHelper on LocalAlbumEntityData { + LocalAlbum toDto({int assetCount = 0}) { + return LocalAlbum( + id: id, + name: name, + updatedAt: updatedAt, + assetCount: assetCount, + backupSelection: backupSelection, + ); + } +} + +extension LocalAssetEntityDataHelper on LocalAssetEntityData { + LocalAsset toDto() { + return LocalAsset( + id: id, + name: name, + checksum: checksum, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + isFavorite: isFavorite, + ); + } +} diff --git a/mobile/lib/utils/datetime_comparison.dart b/mobile/lib/utils/datetime_comparison.dart index 8c53ea45ba..f8ddcfea11 100644 --- a/mobile/lib/utils/datetime_comparison.dart +++ b/mobile/lib/utils/datetime_comparison.dart @@ -1,3 +1,2 @@ bool isAtSameMomentAs(DateTime? a, DateTime? b) => - (a == null && b == null) || - ((a != null && b != null) && a.isAtSameMomentAs(b)); + (a == null && b == null) || ((a != null && b != null) && a.isAtSameMomentAs(b)); diff --git a/mobile/lib/utils/debounce.dart b/mobile/lib/utils/debounce.dart index 78870151a6..e8d3e6fb24 100644 --- a/mobile/lib/utils/debounce.dart +++ b/mobile/lib/utils/debounce.dart @@ -19,8 +19,7 @@ class Debouncer { if (maxWaitTime != null && // _actionFuture == null && // TODO: should this check be here? - (_lastActionTime == null || - DateTime.now().difference(_lastActionTime!) > maxWaitTime!)) { + (_lastActionTime == null || DateTime.now().difference(_lastActionTime!) > maxWaitTime!)) { _callAndRest(); return; } @@ -60,8 +59,7 @@ class Debouncer { _actionFuture = null; } - bool get isActive => - _actionFuture != null || (_timer != null && _timer!.isActive); + bool get isActive => _actionFuture != null || (_timer != null && _timer!.isActive); } /// Creates a [Debouncer] that will be disposed automatically. If no [interval] is provided, a diff --git a/mobile/lib/utils/download.dart b/mobile/lib/utils/download.dart deleted file mode 100644 index c701f353a2..0000000000 --- a/mobile/lib/utils/download.dart +++ /dev/null @@ -1,3 +0,0 @@ -const downloadGroupImage = 'group_image'; -const downloadGroupVideo = 'group_video'; -const downloadGroupLivePhoto = 'group_livephoto'; diff --git a/mobile/lib/utils/draggable_scroll_controller.dart b/mobile/lib/utils/draggable_scroll_controller.dart index 1d22905d1f..cd7ae5b0e1 100644 --- a/mobile/lib/utils/draggable_scroll_controller.dart +++ b/mobile/lib/utils/draggable_scroll_controller.dart @@ -15,19 +15,18 @@ DraggableScrollableController useDraggableScrollController({ ); } -class _DraggableScrollControllerHook - extends Hook { +class _DraggableScrollControllerHook extends Hook { const _DraggableScrollControllerHook({ super.keys, }); @override - HookState> - createState() => _DraggableScrollControllerHookState(); + HookState> createState() => + _DraggableScrollControllerHookState(); } -class _DraggableScrollControllerHookState extends HookState< - DraggableScrollableController, _DraggableScrollControllerHook> { +class _DraggableScrollControllerHookState + extends HookState { late final controller = DraggableScrollableController(); @override diff --git a/mobile/lib/utils/hooks/blurhash_hook.dart b/mobile/lib/utils/hooks/blurhash_hook.dart index 9231e2d972..62208c4cf5 100644 --- a/mobile/lib/utils/hooks/blurhash_hook.dart +++ b/mobile/lib/utils/hooks/blurhash_hook.dart @@ -1,6 +1,7 @@ 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; @@ -15,3 +16,15 @@ ObjectRef useBlurHashRef(Asset? asset) { return useRef(thumbhash.rgbaToBmp(rbga)); } + +ObjectRef useDriftBlurHashRef(RemoteAsset? asset) { + if (asset?.thumbHash == null) { + return useRef(null); + } + + final rbga = thumbhash.thumbHashToRGBA( + base64Decode(asset!.thumbHash!), + ); + + return useRef(thumbhash.rgbaToBmp(rbga)); +} diff --git a/mobile/lib/utils/hooks/timer_hook.dart b/mobile/lib/utils/hooks/timer_hook.dart index a78fed42c3..577e46f5d4 100644 --- a/mobile/lib/utils/hooks/timer_hook.dart +++ b/mobile/lib/utils/hooks/timer_hook.dart @@ -23,8 +23,7 @@ class _TimerHook extends Hook { required this.callback, }); @override - HookState> createState() => - _TimerHookState(); + HookState> createState() => _TimerHookState(); } class _TimerHookState extends HookState { diff --git a/mobile/lib/utils/http_ssl_options.dart b/mobile/lib/utils/http_ssl_options.dart index 04c01d36d9..eaf6e77e4a 100644 --- a/mobile/lib/utils/http_ssl_options.dart +++ b/mobile/lib/utils/http_ssl_options.dart @@ -10,18 +10,17 @@ import 'package:logging/logging.dart'; class HttpSSLOptions { static const MethodChannel _channel = MethodChannel('immich/httpSSLOptions'); - static void apply() { + static void apply({bool applyNative = true}) { AppSettingsEnum setting = AppSettingsEnum.allowSelfSignedSSLCert; - bool allowSelfSignedSSLCert = - Store.get(setting.storeKey as StoreKey, setting.defaultValue); - _apply(allowSelfSignedSSLCert); + bool allowSelfSignedSSLCert = Store.get(setting.storeKey as StoreKey, setting.defaultValue); + _apply(allowSelfSignedSSLCert, applyNative: applyNative); } static void applyFromSettings(bool newValue) { _apply(newValue); } - static void _apply(bool allowSelfSignedSSLCert) { + static void _apply(bool allowSelfSignedSSLCert, {bool applyNative = true}) { String? serverHost; if (allowSelfSignedSSLCert && Store.tryGet(StoreKey.currentUser) != null) { serverHost = Uri.parse(Store.tryGet(StoreKey.serverEndpoint) ?? "").host; @@ -29,10 +28,9 @@ class HttpSSLOptions { SSLClientCertStoreVal? clientCert = SSLClientCertStoreVal.load(); - HttpOverrides.global = - HttpSSLCertOverride(allowSelfSignedSSLCert, serverHost, clientCert); + HttpOverrides.global = HttpSSLCertOverride(allowSelfSignedSSLCert, serverHost, clientCert); - if (Platform.isAndroid) { + if (applyNative && Platform.isAndroid) { _channel.invokeMethod("apply", [ allowSelfSignedSSLCert, serverHost, diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index 50218eaffd..bde50f3a90 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -73,6 +73,9 @@ String getThumbnailUrlForRemoteId( return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}'; } +String getPreviewUrlForRemoteId(final String id) => + '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${AssetMediaSize.preview}'; + String getPlaybackUrlForRemoteId(final String id) { return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/video/playback?'; } diff --git a/mobile/lib/utils/immich_loading_overlay.dart b/mobile/lib/utils/immich_loading_overlay.dart index fcc47b1542..b44f78e0bd 100644 --- a/mobile/lib/utils/immich_loading_overlay.dart +++ b/mobile/lib/utils/immich_loading_overlay.dart @@ -7,8 +7,7 @@ final _loadingEntry = OverlayEntry( builder: (context) => SizedBox.square( dimension: double.infinity, child: DecoratedBox( - decoration: - BoxDecoration(color: context.colorScheme.surface.withAlpha(200)), + decoration: BoxDecoration(color: context.colorScheme.surface.withAlpha(200)), child: const Center( child: DelayedLoadingIndicator( delay: Duration(seconds: 1), @@ -30,8 +29,7 @@ class _LoadingOverlay extends Hook> { _LoadingOverlayState createState() => _LoadingOverlayState(); } -class _LoadingOverlayState - extends HookState, _LoadingOverlay> { +class _LoadingOverlayState extends HookState, _LoadingOverlay> { late final _isLoading = ValueNotifier(false)..addListener(_listener); OverlayEntry? _loadingOverlay; diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index 6b20fa7f37..7f8e8510d3 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:ui'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; @@ -8,6 +9,7 @@ import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; +import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:logging/logging.dart'; import 'package:worker_manager/worker_manager.dart'; @@ -15,8 +17,7 @@ class InvalidIsolateUsageException implements Exception { const InvalidIsolateUsageException(); @override - String toString() => - "IsolateHelper should only be used from the root isolate"; + String toString() => "IsolateHelper should only be used from the root isolate"; } // !! Should be used only from the root isolate @@ -47,6 +48,7 @@ Cancelable runInIsolateGentle({ Logger log = Logger("IsolateLogger"); try { + HttpSSLOptions.apply(applyNative: false); return await computation(ref); } on CanceledError { log.warning( @@ -59,9 +61,28 @@ Cancelable runInIsolateGentle({ stack, ); } finally { - await LogService.I.flushBuffer(); - ref.read(driftProvider).close(); - ref.read(isarProvider).close(); + try { + await LogService.I.flushBuffer(); + await ref.read(driftProvider).close(); + + // Close Isar safely + try { + final isar = ref.read(isarProvider); + if (isar.isOpen) { + await isar.close(); + } + } catch (e) { + debugPrint("Error closing Isar: $e"); + } + + ref.dispose(); + } catch (error) { + debugPrint("Error closing resources in isolate: $error"); + } finally { + ref.dispose(); + // Delay to ensure all resources are released + await Future.delayed(const Duration(seconds: 2)); + } } return null; }); diff --git a/mobile/lib/utils/licenses.dart b/mobile/lib/utils/licenses.dart new file mode 100644 index 0000000000..5ebc2c7b1a --- /dev/null +++ b/mobile/lib/utils/licenses.dart @@ -0,0 +1,42 @@ +const nonPubLicenses = { + 'aves': ''' +BSD 3-Clause License + +Copyright (c) 2020, Thibault Deckers +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''', + 'photo_view': ''' +Copyright 2024 Renan C. AraÃējo + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +''', +}; diff --git a/mobile/lib/utils/map_utils.dart b/mobile/lib/utils/map_utils.dart index df1ff28d8f..3dd849a044 100644 --- a/mobile/lib/utils/map_utils.dart +++ b/mobile/lib/utils/map_utils.dart @@ -7,7 +7,7 @@ import 'package:logging/logging.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; class MapUtils { - MapUtils._(); + const MapUtils._(); static final Logger _log = Logger("MapUtils"); static const defaultSourceId = 'asset-map-markers'; @@ -91,12 +91,9 @@ class MapUtils { } } - if (permission == LocationPermission.denied || - permission == LocationPermission.deniedForever) { + if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { // Open app settings only if you did not request for permission before - if (permission == LocationPermission.deniedForever && - !shouldRequestPermission && - !silent) { + if (permission == LocationPermission.deniedForever && !shouldRequestPermission && !silent) { await Geolocator.openAppSettings(); } return (null, LocationPermission.deniedForever); diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 6bcf92d402..7b61e5521d 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -2,28 +2,35 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/utils/background_sync.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/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/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; +import 'package:logging/logging.dart'; // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; -const int targetVersion = 12; +const int targetVersion = 13; Future migrateDatabaseIfNeeded(Isar db) async { final int version = Store.get(StoreKey.version, targetVersion); @@ -36,8 +43,7 @@ Future migrateDatabaseIfNeeded(Isar db) async { 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)); + await db.storeValues.put(StoreValue(StoreKey.currentUser.id, strValue: user?.id)); }); } } @@ -48,22 +54,18 @@ Future migrateDatabaseIfNeeded(Isar db) async { await _migrateDeviceAsset(db); } - if (version < 12 && (!kReleaseMode)) { - final backgroundSync = BackgroundSyncManager(); - await backgroundSync.syncLocal(); - final drift = Drift(); - await _migrateDeviceAssetToSqlite(db, drift); - await drift.close(); + if (version < 13) { + await Store.put(StoreKey.photoManagerCustomFilter, true); + } + + if (targetVersion >= 12) { + await Store.put(StoreKey.version, targetVersion); + return; } final shouldTruncate = version < 8 || version < targetVersion; if (shouldTruncate) { - if (targetVersion == 12) { - await Store.put(StoreKey.version, targetVersion); - return; - } - await _migrateTo(db, targetVersion); } } @@ -85,9 +87,7 @@ Future _migrateDeviceAsset(Isar db) async { ? (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(); + : (await db.iOSDeviceAssets.where().findAll()).map((i) => _DeviceAsset(assetId: i.id, hash: i.hash)).toList(); final PermissionState ps = await PhotoManager.requestPermissionExtend(); if (!ps.hasAccess) { @@ -101,14 +101,10 @@ Future _migrateDeviceAsset(Isar db) async { } List<_DeviceAsset> localAssets = []; - final List paths = - await PhotoManager.getAssetPathList(onlyAll: true); + 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()) + localAssets = (await db.assets.where().anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId)).findAll()) .map( (a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt), ) @@ -117,12 +113,9 @@ Future _migrateDeviceAsset(Isar db) async { final AssetPathEntity albumWithAll = paths.first; final int assetCount = await albumWithAll.assetCountAsync; - final List allDeviceAssets = - await albumWithAll.getAssetListRange(start: 0, end: assetCount); + final List allDeviceAssets = await albumWithAll.getAssetListRange(start: 0, end: assetCount); - localAssets = allDeviceAssets - .map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)) - .toList(); + localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList(); } debugPrint("[MIGRATION] Device Asset Ids length - ${ids.length}"); @@ -171,33 +164,85 @@ Future _migrateDeviceAsset(Isar db) async { }); } -Future _migrateDeviceAssetToSqlite(Isar db, Drift drift) async { +Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { try { - final isarDeviceAssets = - await db.deviceAssetEntitys.where().sortByAssetId().findAll(); + final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); await drift.batch((batch) { for (final deviceAsset in isarDeviceAssets) { - final companion = LocalAssetEntityCompanion( - updatedAt: Value(deviceAsset.modifiedTime), - id: Value(deviceAsset.assetId), - checksum: Value(base64.encode(deviceAsset.hash)), - ); - batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>( + batch.update( drift.localAssetEntity, - companion, - onConflict: DoUpdate( - (_) => companion, - where: (old) => old.updatedAt.equals(deviceAsset.modifiedTime), + LocalAssetEntityCompanion( + checksum: Value(base64.encode(deviceAsset.hash)), ), + where: (t) => t.id.equals(deviceAsset.assetId), ); } }); } catch (error) { - if (kDebugMode) { - debugPrint( - "[MIGRATION] Error while migrating device assets to SQLite: $error", - ); + debugPrint( + "[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) { + debugPrint( + "[MIGRATION] Error while migrating backup albums to SQLite: $error", + ); } } @@ -208,3 +253,18 @@ class _DeviceAsset { const _DeviceAsset({required this.assetId, this.hash, this.dateTime}); } + +Future runNewSync(WidgetRef ref, {bool full = false}) async { + ref.read(backupProvider.notifier).cancelBackup(); + + final backgroundManager = ref.read(backgroundSyncProvider); + Future.wait([ + backgroundManager.syncLocal(full: full).then( + (_) { + Logger("runNewSync").fine("Hashing assets after syncLocal"); + backgroundManager.hashAssets(); + }, + ), + backgroundManager.syncRemote(), + ]); +} diff --git a/mobile/lib/utils/provider_utils.dart b/mobile/lib/utils/provider_utils.dart index bf18d24213..7af0e61d3c 100644 --- a/mobile/lib/utils/provider_utils.dart +++ b/mobile/lib/utils/provider_utils.dart @@ -1,4 +1,5 @@ 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'; @@ -15,4 +16,5 @@ void invalidateAllApiRepositoryProviders(WidgetRef ref) { ref.invalidate(personApiRepositoryProvider); ref.invalidate(assetApiRepositoryProvider); ref.invalidate(timelineRepositoryProvider); + ref.invalidate(searchApiRepositoryProvider); } diff --git a/mobile/lib/utils/remote_album.utils.dart b/mobile/lib/utils/remote_album.utils.dart new file mode 100644 index 0000000000..af853e08d2 --- /dev/null +++ b/mobile/lib/utils/remote_album.utils.dart @@ -0,0 +1,91 @@ +import 'package:collection/collection.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; + +typedef AlbumSortFn = List Function( + List albums, + bool isReverse, +); + +class _RemoteAlbumSortHandlers { + const _RemoteAlbumSortHandlers._(); + + 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.updatedAt); + 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) { + // For most recent, we sort by updatedAt in descending order + return b.updatedAt.compareTo(a.updatedAt); + }); + return (isReverse ? sorted.reversed : sorted).toList(); + } + + static const AlbumSortFn mostOldest = _sortByMostOldest; + static List _sortByMostOldest( + List albums, + bool isReverse, + ) { + final sorted = albums.sorted((a, b) { + // For oldest, we sort by createdAt in ascending order + return a.createdAt.compareTo(b.createdAt); + }); + return (isReverse ? sorted.reversed : sorted).toList(); + } +} + +enum RemoteAlbumSortMode { + title("library_page_sort_title", _RemoteAlbumSortHandlers.title), + assetCount( + "library_page_sort_asset_count", + _RemoteAlbumSortHandlers.assetCount, + ), + lastModified( + "library_page_sort_last_modified", + _RemoteAlbumSortHandlers.lastModified, + ), + created("library_page_sort_created", _RemoteAlbumSortHandlers.created), + mostRecent("sort_recent", _RemoteAlbumSortHandlers.mostRecent), + mostOldest("sort_oldest", _RemoteAlbumSortHandlers.mostOldest); + + final String key; + final AlbumSortFn sortFn; + + const RemoteAlbumSortMode(this.key, this.sortFn); +} diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart index a5466c83a2..633ce2463a 100644 --- a/mobile/lib/utils/selection_handlers.dart +++ b/mobile/lib/utils/selection_handlers.dart @@ -24,10 +24,7 @@ void handleShareAssets( showDialog( context: context, builder: (BuildContext buildContext) { - ref - .watch(shareServiceProvider) - .shareAssets(selection.toList(), context) - .then( + ref.watch(shareServiceProvider).shareAssets(selection.toList(), context).then( (bool status) { if (!status) { ImmichToast.show( @@ -56,14 +53,10 @@ Future handleArchiveAssets( }) async { if (selection.isNotEmpty) { shouldArchive ??= !selection.every((a) => a.isArchived); - await ref - .read(assetProvider.notifier) - .toggleArchive(selection, shouldArchive); + 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}); + ? '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, @@ -83,9 +76,7 @@ Future handleFavoriteAssets( }) async { if (selection.isNotEmpty) { shouldFavorite ??= !selection.every((a) => a.isFavorite); - await ref - .watch(assetProvider.notifier) - .toggleFavorite(selection, shouldFavorite); + await ref.watch(assetProvider.notifier).toggleFavorite(selection, shouldFavorite); final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; final toastMessage = shouldFavorite @@ -140,8 +131,7 @@ Future handleEditLocation( 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) { + if (assetWithExif.exifInfo?.latitude != null && assetWithExif.exifInfo?.longitude != null) { initialLatLng = LatLng( assetWithExif.exifInfo!.latitude!, assetWithExif.exifInfo!.longitude!, @@ -168,9 +158,7 @@ Future handleSetAssetsVisibility( List selection, ) async { if (selection.isNotEmpty) { - await ref - .watch(assetProvider.notifier) - .setLockedView(selection, visibility); + await ref.watch(assetProvider.notifier).setLockedView(selection, visibility); final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; final toastMessage = visibility == AssetVisibilityEnum.locked diff --git a/mobile/lib/utils/storage_indicator.dart b/mobile/lib/utils/storage_indicator.dart deleted file mode 100644 index a7dad063ca..0000000000 --- a/mobile/lib/utils/storage_indicator.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -/// Returns the suitable [IconData] to represent an [Asset]s storage location -IconData storageIcon(Asset asset) => switch (asset.storage) { - AssetState.local => Icons.cloud_off_outlined, - AssetState.remote => Icons.cloud_outlined, - AssetState.merged => Icons.cloud_done_outlined, - }; diff --git a/mobile/lib/utils/throttle.dart b/mobile/lib/utils/throttle.dart index bc0dcf9e2f..c4427472d3 100644 --- a/mobile/lib/utils/throttle.dart +++ b/mobile/lib/utils/throttle.dart @@ -9,8 +9,7 @@ class Throttler { Throttler({required this.interval}); T? run(T Function() action) { - if (_lastActionTime == null || - (DateTime.now().difference(_lastActionTime!) > interval)) { + if (_lastActionTime == null || (DateTime.now().difference(_lastActionTime!) > interval)) { final response = action(); _lastActionTime = DateTime.now(); return response; diff --git a/mobile/lib/utils/thumbnail_utils.dart b/mobile/lib/utils/thumbnail_utils.dart index 33dd916980..758305c8bc 100644 --- a/mobile/lib/utils/thumbnail_utils.dart +++ b/mobile/lib/utils/thumbnail_utils.dart @@ -12,8 +12,7 @@ String getAltText( if (exifInfo?.description != null && exifInfo!.description!.isNotEmpty) { return exifInfo.description!; } - final (template, args) = - getAltTextTemplate(exifInfo, fileCreatedAt, type, peopleNames); + final (template, args) = getAltTextTemplate(exifInfo, fileCreatedAt, type, peopleNames); return template.t(args: args); } diff --git a/mobile/lib/utils/upload.dart b/mobile/lib/utils/upload.dart deleted file mode 100644 index a0b77f1d93..0000000000 --- a/mobile/lib/utils/upload.dart +++ /dev/null @@ -1 +0,0 @@ -const uploadGroup = 'upload_group'; diff --git a/mobile/lib/utils/url_helper.dart b/mobile/lib/utils/url_helper.dart index 187026b53c..ec65b4f7ee 100644 --- a/mobile/lib/utils/url_helper.dart +++ b/mobile/lib/utils/url_helper.dart @@ -4,8 +4,7 @@ import 'package:punycode/punycode.dart'; String sanitizeUrl(String url) { // Add schema if none is set - final urlWithSchema = - url.trimLeft().startsWith(RegExp(r"https?://")) ? url : "https://$url"; + final urlWithSchema = url.trimLeft().startsWith(RegExp(r"https?://")) ? url : "https://$url"; // Remove trailing slash(es) return urlWithSchema.trimRight().replaceFirst(RegExp(r"/+$"), ""); diff --git a/mobile/lib/widgets/activities/activity_text_field.dart b/mobile/lib/widgets/activities/activity_text_field.dart index ce4f1364a3..f111de5e53 100644 --- a/mobile/lib/widgets/activities/activity_text_field.dart +++ b/mobile/lib/widgets/activities/activity_text_field.dart @@ -24,8 +24,7 @@ class ActivityTextField extends HookConsumerWidget { 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 activityNotifier = ref.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); final user = ref.watch(currentUserProvider); final inputController = useTextEditingController(); final inputFocusNode = useFocusNode(); @@ -88,9 +87,7 @@ class ActivityTextField extends HookConsumerWidget { ), ), suffixIconColor: liked ? Colors.red[700] : null, - hintText: !isEnabled - ? 'shared_album_activities_input_disable'.tr() - : 'say_something'.tr(), + hintText: !isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(), hintStyle: TextStyle( fontWeight: FontWeight.normal, fontSize: 14, diff --git a/mobile/lib/widgets/activities/activity_tile.dart b/mobile/lib/widgets/activities/activity_tile.dart index 2dd16b73cb..a2bc5135c6 100644 --- a/mobile/lib/widgets/activities/activity_tile.dart +++ b/mobile/lib/widgets/activities/activity_tile.dart @@ -38,11 +38,8 @@ class ActivityTile extends HookConsumerWidget { leftAlign: isLike || showAssetThumbnail, ), // No subtitle for like, so center title - titleAlignment: - !isLike ? ListTileTitleAlignment.top : ListTileTitleAlignment.center, - trailing: showAssetThumbnail - ? _ActivityAssetThumbnail(activity.assetId!) - : null, + titleAlignment: !isLike ? ListTileTitleAlignment.top : ListTileTitleAlignment.center, + trailing: showAssetThumbnail ? _ActivityAssetThumbnail(activity.assetId!) : null, subtitle: !isLike ? Text(activity.comment!) : null, ); } @@ -62,12 +59,10 @@ class _ActivityTitle extends StatelessWidget { @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)); + final textStyle = context.textTheme.bodyMedium?.copyWith(color: textColor.withValues(alpha: 0.6)); return Row( - mainAxisAlignment: - leftAlign ? MainAxisAlignment.start : MainAxisAlignment.spaceBetween, + mainAxisAlignment: leftAlign ? MainAxisAlignment.start : MainAxisAlignment.spaceBetween, mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max, children: [ Text( diff --git a/mobile/lib/widgets/album/add_to_album_sliverlist.dart b/mobile/lib/widgets/album/add_to_album_sliverlist.dart index 3472e2179b..b0f2a0a49a 100644 --- a/mobile/lib/widgets/album/add_to_album_sliverlist.dart +++ b/mobile/lib/widgets/album/add_to_album_sliverlist.dart @@ -25,13 +25,11 @@ class AddToAlbumSliverList extends HookConsumerWidget { final albumSortMode = ref.watch(albumSortByOptionsProvider); final albumSortIsReverse = ref.watch(albumSortOrderProvider); final sortedAlbums = albumSortMode.sortFn(albums, albumSortIsReverse); - final sortedSharedAlbums = - albumSortMode.sortFn(sharedAlbums, albumSortIsReverse); + final sortedSharedAlbums = albumSortMode.sortFn(sharedAlbums, albumSortIsReverse); return SliverList( - delegate: SliverChildBuilderDelegate( - childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), - (context, index) { + delegate: + SliverChildBuilderDelegate(childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), (context, index) { // Build shared expander if (index == 0 && sortedSharedAlbums.isNotEmpty) { return Padding( @@ -47,9 +45,7 @@ class AddToAlbumSliverList extends HookConsumerWidget { itemCount: sortedSharedAlbums.length, itemBuilder: (context, index) => AlbumThumbnailListTile( album: sortedSharedAlbums[index], - onTap: enabled - ? () => onAddToAlbum(sortedSharedAlbums[index]) - : () {}, + onTap: enabled ? () => onAddToAlbum(sortedSharedAlbums[index]) : () {}, ), ), ], diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index 5e89cd7db3..e8d0425d4e 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -106,10 +106,10 @@ class AlbumThumbnailCard extends ConsumerWidget { width: cardSize, height: cardSize, child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: album.thumbnail.value == null - ? buildEmptyThumbnail() - : buildAlbumThumbnail(), + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), + child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(), ), ), if (showTitle) ...[ diff --git a/mobile/lib/widgets/album/album_thumbnail_listtile.dart b/mobile/lib/widgets/album/album_thumbnail_listtile.dart index f35d4b7ede..8332cde889 100644 --- a/mobile/lib/widgets/album/album_thumbnail_listtile.dart +++ b/mobile/lib/widgets/album/album_thumbnail_listtile.dart @@ -50,10 +50,8 @@ class AlbumThumbnailListTile extends StatelessWidget { type: AssetMediaSize.thumbnail, ), httpHeaders: ApiService.getRequestHeaders(), - cacheKey: - getAlbumThumbNailCacheKey(album, type: AssetMediaSize.thumbnail), - errorWidget: (context, url, error) => - const Icon(Icons.image_not_supported_outlined), + cacheKey: getAlbumThumbNailCacheKey(album, type: AssetMediaSize.thumbnail), + errorWidget: (context, url, error) => const Icon(Icons.image_not_supported_outlined), ); } @@ -70,9 +68,7 @@ class AlbumThumbnailListTile extends StatelessWidget { children: [ ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), - child: album.thumbnail.value == null - ? buildEmptyThumbnail() - : buildAlbumThumbnail(), + child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(), ), Expanded( child: Padding( diff --git a/mobile/lib/widgets/album/album_title_text_field.dart b/mobile/lib/widgets/album/album_title_text_field.dart index c19c827846..7807a6e6ae 100644 --- a/mobile/lib/widgets/album/album_title_text_field.dart +++ b/mobile/lib/widgets/album/album_title_text_field.dart @@ -59,13 +59,17 @@ class AlbumTitleTextField extends ConsumerWidget { splashRadius: 10, ) : null, - enabledBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(10), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.all( + Radius.circular(10), + ), ), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.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( diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart index 14715e40a9..f13f1c3b21 100644 --- a/mobile/lib/widgets/album/album_viewer_appbar.dart +++ b/mobile/lib/widgets/album/album_viewer_appbar.dart @@ -12,8 +12,7 @@ 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 { +class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidget { const AlbumViewerAppbar({ super.key, required this.userId, @@ -53,13 +52,10 @@ class AlbumViewerAppbar extends HookConsumerWidget final newAlbumDescription = albumViewer.editDescriptionText; final isEditAlbum = albumViewer.isEditAlbum; - final comments = album.shared - ? ref.watch(activityStatisticsProvider(album.remoteId!)) - : 0; + final comments = album.shared ? ref.watch(activityStatisticsProvider(album.remoteId!)) : 0; deleteAlbum() async { - final bool success = - await ref.watch(albumProvider.notifier).deleteAlbum(album); + final bool success = await ref.watch(albumProvider.notifier).deleteAlbum(album); context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])); @@ -112,8 +108,7 @@ class AlbumViewerAppbar extends HookConsumerWidget } void onLeaveAlbumPressed() async { - bool isSuccess = - await ref.watch(albumProvider.notifier).leaveAlbum(album); + bool isSuccess = await ref.watch(albumProvider.notifier).leaveAlbum(album); if (isSuccess) { context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])); @@ -152,8 +147,7 @@ class AlbumViewerAppbar extends HookConsumerWidget } void onSortOrderToggled() async { - final updatedAlbum = - await ref.read(albumProvider.notifier).toggleSortOrder(album); + final updatedAlbum = await ref.read(albumProvider.notifier).toggleSortOrder(album); if (updatedAlbum == null) { ImmichToast.show( @@ -241,8 +235,7 @@ class AlbumViewerAppbar extends HookConsumerWidget children: [ ...buildBottomSheetActions(), if (onAddPhotos != null) ...commonActions, - if (onAddPhotos != null && userId == album.ownerId) - ...ownerActions, + if (onAddPhotos != null && userId == album.ownerId) ...ownerActions, ], ), ), @@ -281,9 +274,7 @@ class AlbumViewerAppbar extends HookConsumerWidget return IconButton( onPressed: () async { if (newAlbumTitle.isNotEmpty) { - bool isSuccess = await ref - .watch(albumViewerProvider.notifier) - .changeAlbumTitle(album, newAlbumTitle); + bool isSuccess = await ref.watch(albumViewerProvider.notifier).changeAlbumTitle(album, newAlbumTitle); if (!isSuccess) { ImmichToast.show( context: context, @@ -294,9 +285,8 @@ class AlbumViewerAppbar extends HookConsumerWidget } titleFocusNode.unfocus(); } else if (newAlbumDescription.isNotEmpty) { - bool isSuccessDescription = await ref - .watch(albumViewerProvider.notifier) - .changeAlbumDescription(album, newAlbumDescription); + bool isSuccessDescription = + await ref.watch(albumViewerProvider.notifier).changeAlbumDescription(album, newAlbumDescription); if (!isSuccessDescription) { ImmichToast.show( context: context, @@ -330,8 +320,7 @@ class AlbumViewerAppbar extends HookConsumerWidget leading: buildLeadingButton(), centerTitle: false, actions: [ - if (album.shared && (album.activityEnabled || comments != 0)) - buildActivitiesButton(), + if (album.shared && (album.activityEnabled || comments != 0)) buildActivitiesButton(), if (album.isRemote) ...[ IconButton( splashRadius: 25, diff --git a/mobile/lib/widgets/album/album_viewer_editable_description.dart b/mobile/lib/widgets/album/album_viewer_editable_description.dart index b82e7f3d83..94f41dc7fd 100644 --- a/mobile/lib/widgets/album/album_viewer_editable_description.dart +++ b/mobile/lib/widgets/album/album_viewer_editable_description.dart @@ -19,15 +19,13 @@ class AlbumViewerEditableDescription extends HookConsumerWidget { final albumViewerState = ref.watch(albumViewerProvider); final descriptionTextEditController = useTextEditingController( - text: albumViewerState.isEditAlbum && - albumViewerState.editDescriptionText.isNotEmpty + text: albumViewerState.isEditAlbum && albumViewerState.editDescriptionText.isNotEmpty ? albumViewerState.editDescriptionText : albumDescription, ); void onFocusModeChange() { - if (!descriptionFocusNode.hasFocus && - descriptionTextEditController.text.isEmpty) { + if (!descriptionFocusNode.hasFocus && descriptionTextEditController.text.isEmpty) { ref.watch(albumViewerProvider.notifier).setEditDescriptionText(""); descriptionTextEditController.text = ""; } @@ -49,9 +47,7 @@ class AlbumViewerEditableDescription extends HookConsumerWidget { onChanged: (value) { if (value.isEmpty) { } else { - ref - .watch(albumViewerProvider.notifier) - .setEditDescriptionText(value); + ref.watch(albumViewerProvider.notifier).setEditDescriptionText(value); } }, focusNode: descriptionFocusNode, @@ -62,9 +58,7 @@ class AlbumViewerEditableDescription extends HookConsumerWidget { onTap: () { context.focusScope.requestFocus(descriptionFocusNode); - ref - .watch(albumViewerProvider.notifier) - .setEditDescriptionText(albumDescription); + ref.watch(albumViewerProvider.notifier).setEditDescriptionText(albumDescription); ref.watch(albumViewerProvider.notifier).enableEditAlbum(); if (descriptionTextEditController.text == '') { diff --git a/mobile/lib/widgets/album/album_viewer_editable_title.dart b/mobile/lib/widgets/album/album_viewer_editable_title.dart index 038c9a13d8..b64be09ff9 100644 --- a/mobile/lib/widgets/album/album_viewer_editable_title.dart +++ b/mobile/lib/widgets/album/album_viewer_editable_title.dart @@ -19,8 +19,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { final albumViewerState = ref.watch(albumViewerProvider); final titleTextEditController = useTextEditingController( - text: albumViewerState.isEditAlbum && - albumViewerState.editTitleText.isNotEmpty + text: albumViewerState.isEditAlbum && albumViewerState.editTitleText.isNotEmpty ? albumViewerState.editTitleText : albumName, ); diff --git a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart new file mode 100644 index 0000000000..f7f3f62b32 --- /dev/null +++ b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; +import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; + +class RemoteAlbumSharedUserIcons extends ConsumerWidget { + const RemoteAlbumSharedUserIcons({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentAlbum = ref.watch(currentRemoteAlbumProvider); + if (currentAlbum == null) { + return const SizedBox(); + } + + final sharedUsersAsync = ref.watch(remoteAlbumSharedUsersProvider(currentAlbum.id)); + + return sharedUsersAsync.maybeWhen( + data: (sharedUsers) { + if (sharedUsers.isEmpty) { + return const SizedBox(); + } + + return SizedBox( + height: 50, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemBuilder: ((context, index) { + return Padding( + padding: const EdgeInsets.only(right: 4.0), + child: UserCircleAvatar( + user: sharedUsers[index], + radius: 18, + size: 36, + hasBorder: true, + ), + ); + }), + itemCount: sharedUsers.length, + ), + ); + }, + orElse: () => const SizedBox(), + ); + } +} diff --git a/mobile/lib/widgets/asset_grid/asset_drag_region.dart b/mobile/lib/widgets/asset_grid/asset_drag_region.dart index 6335a1d64d..f27fae64e8 100644 --- a/mobile/lib/widgets/asset_grid/asset_drag_region.dart +++ b/mobile/lib/widgets/asset_grid/asset_drag_region.dart @@ -65,8 +65,7 @@ class _AssetDragRegionState extends State { Widget build(BuildContext context) { return RawGestureDetector( gestures: { - _CustomLongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers< - _CustomLongPressGestureRecognizer>( + _CustomLongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<_CustomLongPressGestureRecognizer>( () => _CustomLongPressGestureRecognizer(), _registerCallbacks, ), @@ -89,9 +88,7 @@ class _AssetDragRegionState extends State { 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?) + return (hitTestResult.path.firstWhereOrNull((hit) => hit.target is _AssetIndexProxy)?.target as _AssetIndexProxy?) ?.index; } @@ -99,8 +96,7 @@ class _AssetDragRegionState extends State { /// 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)) { + if (height != null && (topScrollOffset == null || bottomScrollOffset == null)) { topScrollOffset = height * scrollOffset; bottomScrollOffset = height - topScrollOffset!; } @@ -188,8 +184,7 @@ class AssetIndexWrapper extends SingleChildRenderObjectWidget { // ignore: library_private_types_in_public_api _AssetIndexProxy renderObject, ) { - renderObject.index = - AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex); + renderObject.index = AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex); } } diff --git a/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart b/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart index 64803046ef..88b993a026 100644 --- a/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart +++ b/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart @@ -23,7 +23,7 @@ class RenderAssetGridElement { final int offset; final int totalCount; - RenderAssetGridElement( + const RenderAssetGridElement( this.type, { this.title, required this.date, @@ -53,8 +53,7 @@ class RenderList { /// global offset of assets in [_buf] int _bufOffset = 0; - RenderList(this.elements, this.query, this.allAssets) - : totalAssets = allAssets?.length ?? query!.countSync(); + RenderList(this.elements, this.query, this.allAssets) : totalAssets = allAssets?.length ?? query!.countSync(); bool get isEmpty => totalAssets == 0; @@ -90,9 +89,7 @@ class RenderList { // 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), + 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(); @@ -156,9 +153,7 @@ class RenderList { : null; for (int i = 0; i < total; i += sectionSize) { - final date = assets != null - ? assets[i].fileCreatedAt - : await dateLoader?.getDate(i); + final date = assets != null ? assets[i].fileCreatedAt : await dateLoader?.getDate(i); final int count = i + sectionSize > total ? total - i : sectionSize; if (date == null) break; @@ -175,11 +170,8 @@ class RenderList { return RenderList(elements, query, assets); } - final formatSameYear = - groupBy == GroupAssetsBy.month ? DateFormat.MMMM() : DateFormat.MMMEd(); - final formatOtherYear = groupBy == GroupAssetsBy.month - ? DateFormat.yMMMM() - : DateFormat.yMMMEd(); + 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(); @@ -193,16 +185,9 @@ class RenderList { 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)) { + 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); @@ -212,10 +197,7 @@ class RenderList { } void mergeMonth() { - if (last != null && - groupBy == GroupAssetsBy.auto && - monthCount <= 30 && - elements.length > lastMonthIndex + 1) { + 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]; @@ -233,8 +215,7 @@ class RenderList { } void addElems(DateTime d, DateTime? prevDate) { - final bool newMonth = - last == null || last.year != d.year || last.month != d.month; + final bool newMonth = last == null || last.year != d.year || last.month != d.month; if (newMonth) { mergeMonth(); lastMonthIndex = elements.length; @@ -258,12 +239,8 @@ class RenderList { 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), + ? (d.year == currentYear ? formatSameYear.format(d) : formatOtherYear.format(d)) + : (groupBy == GroupAssetsBy.auto ? formatDateRange(d, prevDate ?? d) : null), ), ); } @@ -277,11 +254,7 @@ class RenderList { // 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(); + : await query!.offset(offset).limit(pageSize).fileCreatedAtProperty().findAll(); int i = 0; for (final date in dates) { final d = DateTime( @@ -357,11 +330,7 @@ class DateBatchLoader { Future _loadBatch(int targetIndex) async { final batchStart = (targetIndex ~/ batchSize) * batchSize; - _buffer = await query - .offset(batchStart) - .limit(batchSize) - .fileCreatedAtProperty() - .findAll(); + _buffer = await query.offset(batchStart).limit(batchSize).fileCreatedAtProperty().findAll(); _bufferStart = batchStart; } diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart index 3283b90b21..e265162850 100644 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart @@ -71,15 +71,11 @@ class ControlBottomAppBar extends HookConsumerWidget { @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 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(); + final sharedAlbums = ref.watch(albumProvider).where((a) => a.shared).toList(); const bottomPadding = 0.24; final scrollController = useDraggableScrollController(); final isInLockedView = ref.watch(inLockedViewProvider); @@ -132,9 +128,7 @@ class ControlBottomAppBar extends HookConsumerWidget { List renderActionButtons() { return [ ControlBoxButton( - iconData: Platform.isAndroid - ? Icons.share_rounded - : Icons.ios_share_rounded, + iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, label: "share".tr(), onPressed: enabled ? () => onShare(true) : null, ), @@ -146,16 +140,13 @@ class ControlBottomAppBar extends HookConsumerWidget { ), if (hasRemote && onArchive != null) ControlBoxButton( - iconData: - unarchive ? Icons.unarchive_outlined : Icons.archive_outlined, + 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, + iconData: unfavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded, label: (unfavorite ? "unfavorite" : "favorite").tr(), onPressed: enabled ? onFavorite : null, ), @@ -174,11 +165,8 @@ class ControlBottomAppBar extends HookConsumerWidget { child: ControlBoxButton( iconData: Icons.delete_sweep_outlined, label: "delete".tr(), - onPressed: enabled - ? () => handleRemoteDelete(!trashEnabled, onDelete!) - : null, - onLongPressed: - enabled ? () => showForceDeleteDialog(onDelete!) : null, + onPressed: enabled ? () => handleRemoteDelete(!trashEnabled, onDelete!) : null, + onLongPressed: enabled ? () => showForceDeleteDialog(onDelete!) : null, ), ), if (hasRemote && onDeleteServer != null && !isInLockedView) @@ -264,18 +252,12 @@ class ControlBottomAppBar extends HookConsumerWidget { 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(), + 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) + if (!selectionAssetState.hasLocal && selectionAssetState.selectedCount > 1 && onStack != null) ConstrainedBox( constraints: const BoxConstraints(maxWidth: 90), child: ControlBoxButton( diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart index 6eddf58adb..ffe8c54320 100644 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart +++ b/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart @@ -210,7 +210,7 @@ class DraggableScrollbar extends StatefulWidget { BoxConstraints? labelConstraints, }) { final scrollThumb = ClipPath( - clipper: ArrowClipper(), + clipper: const ArrowClipper(), child: Container( height: height, width: 20.0, @@ -276,8 +276,7 @@ class ScrollLabel extends StatelessWidget { final Text child; final BoxConstraints? constraints; - static const BoxConstraints _defaultConstraints = - BoxConstraints.tightFor(width: 72.0, height: 28.0); + static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); const ScrollLabel({ super.key, @@ -308,8 +307,7 @@ class ScrollLabel extends StatelessWidget { } } -class DraggableScrollbarState extends State - with TickerProviderStateMixin { +class DraggableScrollbarState extends State with TickerProviderStateMixin { late double _barOffset; late double _viewOffset; late bool _isDragInProcess; @@ -356,8 +354,7 @@ class DraggableScrollbarState extends State super.dispose(); } - double get barMaxScrollExtent => - context.size!.height - widget.heightScrollThumb; + double get barMaxScrollExtent => context.size!.height - widget.heightScrollThumb; double get barMinScrollExtent => 0; @@ -447,8 +444,7 @@ class DraggableScrollbarState extends State } } - if (notification is ScrollUpdateNotification || - notification is OverscrollNotification) { + if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { if (_thumbAnimationController.status != AnimationStatus.forward) { _thumbAnimationController.forward(); } @@ -570,6 +566,7 @@ class ArrowCustomPainter extends CustomPainter { ///This cut 2 lines in arrow shape class ArrowClipper extends CustomClipper { + const ArrowClipper(); @override Path getClip(Size size) { Path path = Path(); @@ -626,8 +623,7 @@ class SlideFadeTransition extends StatelessWidget { Widget build(BuildContext context) { return AnimatedBuilder( animation: animation, - builder: (context, child) => - animation.value == 0.0 ? const SizedBox() : child!, + builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!, child: SlideTransition( position: Tween( begin: const Offset(0.3, 0.0), diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart index 746bbde6ef..63fa3be763 100644 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart +++ b/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart @@ -169,8 +169,7 @@ class ScrollLabel extends StatelessWidget { final Text child; final BoxConstraints? constraints; - static const BoxConstraints _defaultConstraints = - BoxConstraints.tightFor(width: 72.0, height: 28.0); + static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); const ScrollLabel({ super.key, @@ -202,8 +201,7 @@ class ScrollLabel extends StatelessWidget { } } -class DraggableScrollbarState extends State - with TickerProviderStateMixin { +class DraggableScrollbarState extends State with TickerProviderStateMixin { late double _barOffset; late bool _isDragInProcess; late int _currentItem; @@ -250,10 +248,7 @@ class DraggableScrollbarState extends State super.dispose(); } - double get barMaxScrollExtent => - (context.size?.height ?? 0) - - widget.heightScrollThumb - - (widget.heightOffset ?? 0); + double get barMaxScrollExtent => (context.size?.height ?? 0) - widget.heightScrollThumb - (widget.heightOffset ?? 0); double get barMinScrollExtent => 0; @@ -317,8 +312,7 @@ class DraggableScrollbarState extends State setState(() { try { - int firstItemIndex = - widget.itemPositionsListener.itemPositions.value.first.index; + int firstItemIndex = widget.itemPositionsListener.itemPositions.value.first.index; if (notification is ScrollUpdateNotification) { _barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent; @@ -331,8 +325,7 @@ class DraggableScrollbarState extends State } } - if (notification is ScrollUpdateNotification || - notification is OverscrollNotification) { + if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { if (_thumbAnimationController.status != AnimationStatus.forward) { _thumbAnimationController.forward(); } @@ -479,6 +472,7 @@ class ArrowCustomPainter extends CustomPainter { ///This cut 2 lines in arrow shape class ArrowClipper extends CustomClipper { + const ArrowClipper(); @override Path getClip(Size size) { Path path = Path(); @@ -535,8 +529,7 @@ class SlideFadeTransition extends StatelessWidget { Widget build(BuildContext context) { return AnimatedBuilder( animation: animation, - builder: (context, child) => - animation.value == 0.0 ? const SizedBox() : child!, + builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!, child: SlideTransition( position: Tween( begin: const Offset(0.3, 0.0), diff --git a/mobile/lib/widgets/asset_grid/group_divider_title.dart b/mobile/lib/widgets/asset_grid/group_divider_title.dart index b9fe8e3c1d..81f9392d37 100644 --- a/mobile/lib/widgets/asset_grid/group_divider_title.dart +++ b/mobile/lib/widgets/asset_grid/group_divider_title.dart @@ -32,8 +32,7 @@ class GroupDividerTitle extends HookConsumerWidget { useEffect( () { - groupBy.value = GroupAssetsBy.values[ - appSettingService.getSetting(AppSettingsEnum.groupAssetsBy)]; + groupBy.value = GroupAssetsBy.values[appSettingService.getSetting(AppSettingsEnum.groupAssetsBy)]; return null; }, [], @@ -75,14 +74,12 @@ class GroupDividerTitle extends HookConsumerWidget { ? Icon( Icons.check_circle_rounded, color: context.primaryColor, - semanticLabel: - "unselect_all_in".tr(namedArgs: {"group": text}), + 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}), + 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 index da4c47e466..112d8074ad 100644 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/widgets/asset_grid/immich_asset_grid.dart @@ -27,8 +27,7 @@ class ImmichAssetGrid extends HookConsumerWidget { final bool canDeselect; final bool? dynamicLayout; final bool showMultiSelectIndicator; - final void Function(Iterable itemPositions)? - visibleItemsListener; + final void Function(Iterable itemPositions)? visibleItemsListener; final Widget? topWidget; final bool shrinkWrap; final bool showDragScroll; @@ -82,10 +81,8 @@ class ImmichAssetGrid extends HookConsumerWidget { Widget buildAssetGridView(RenderList renderList) { return RawGestureDetector( gestures: { - CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers< - CustomScaleGestureRecognizer>( - () => CustomScaleGestureRecognizer(), - (CustomScaleGestureRecognizer scale) { + CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => CustomScaleGestureRecognizer(), (CustomScaleGestureRecognizer scale) { scale.onStart = (details) { baseScaleFactor.value = scaleFactor.value; }; @@ -106,15 +103,13 @@ class ImmichAssetGrid extends HookConsumerWidget { onRefresh: onRefresh, assetsPerRow: perRow.value, listener: listener, - showStorageIndicator: showStorageIndicator ?? - settings.getSetting(AppSettingsEnum.storageIndicator), + showStorageIndicator: showStorageIndicator ?? settings.getSetting(AppSettingsEnum.storageIndicator), renderList: renderList, margin: margin, selectionActive: selectionActive, preselectedAssets: preselectedAssets, canDeselect: canDeselect, - dynamicLayout: dynamicLayout ?? - settings.getSetting(AppSettingsEnum.dynamicLayout), + dynamicLayout: dynamicLayout ?? settings.getSetting(AppSettingsEnum.dynamicLayout), showMultiSelectIndicator: showMultiSelectIndicator, visibleItemsListener: visibleItemsListener, topWidget: topWidget, diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart index 060898e270..ccea8307ae 100644 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart @@ -51,8 +51,7 @@ class ImmichAssetGridView extends ConsumerStatefulWidget { final bool canDeselect; final bool dynamicLayout; final bool showMultiSelectIndicator; - final void Function(Iterable itemPositions)? - visibleItemsListener; + final void Function(Iterable itemPositions)? visibleItemsListener; final Widget? topWidget; final int heroOffset; final bool shrinkWrap; @@ -90,24 +89,20 @@ class ImmichAssetGridView extends ConsumerStatefulWidget { class ImmichAssetGridViewState extends ConsumerState { final ItemScrollController _itemScrollController = ItemScrollController(); - final ScrollOffsetController _scrollOffsetController = - ScrollOffsetController(); - final ItemPositionsListener _itemPositionsListener = - ItemPositionsListener.create(); + 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); + 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); + final Set _draggedAssets = HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id); ScrollPhysics? _scrollPhysics; @@ -131,9 +126,7 @@ class ImmichAssetGridViewState extends ConsumerState { void _deselectAssets(List assets) { final assetsToDeselect = assets.where( - (a) => - widget.canDeselect || - !(widget.preselectedAssets?.contains(a) ?? false), + (a) => widget.canDeselect || !(widget.preselectedAssets?.contains(a) ?? false), ); setState(() { @@ -152,9 +145,7 @@ class ImmichAssetGridViewState extends ConsumerState { _dragAnchorSectionIndex = null; _draggedAssets.clear(); _dragging = false; - if (!widget.canDeselect && - widget.preselectedAssets != null && - widget.preselectedAssets!.isNotEmpty) { + if (!widget.canDeselect && widget.preselectedAssets != null && widget.preselectedAssets!.isNotEmpty) { _selectedAssets.addAll(widget.preselectedAssets!); } _callSelectionListener(false); @@ -162,8 +153,7 @@ class ImmichAssetGridViewState extends ConsumerState { } bool _allAssetsSelected(List assets) { - return widget.selectionActive && - assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null; + return widget.selectionActive && assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null; } Future _scrollToIndex(int index) async { @@ -244,8 +234,7 @@ class ImmichAssetGridViewState extends ConsumerState { } Widget _buildAssetGrid() { - final useDragScrolling = - widget.showDragScroll && widget.renderList.totalAssets >= 20; + final useDragScrolling = widget.showDragScroll && widget.renderList.totalAssets >= 20; void dragScrolling(bool active) { if (active != _scrolling) { @@ -256,9 +245,7 @@ class ImmichAssetGridViewState extends ConsumerState { } bool appBarOffset() { - return (ref.watch(tabProvider).index == 0 && - ModalRoute.of(context)?.settings.name == - TabControllerRoute.name) || + return (ref.watch(tabProvider).index == 0 && ModalRoute.of(context)?.settings.name == TabControllerRoute.name) || (ModalRoute.of(context)?.settings.name == AlbumViewerRoute.name); } @@ -272,8 +259,7 @@ class ImmichAssetGridViewState extends ConsumerState { physics: _scrollPhysics, itemScrollController: _itemScrollController, scrollOffsetController: _scrollOffsetController, - itemCount: widget.renderList.elements.length + - (widget.topWidget != null ? 1 : 0), + itemCount: widget.renderList.elements.length + (widget.topWidget != null ? 1 : 0), addRepaintBoundaries: true, shrinkWrap: widget.shrinkWrap, ); @@ -283,13 +269,10 @@ class ImmichAssetGridViewState extends ConsumerState { scrollStateListener: dragScrolling, itemPositionsListener: _itemPositionsListener, controller: _itemScrollController, - backgroundColor: context.isDarkTheme - ? context.colorScheme.primary.darken(amount: .5) - : context.colorScheme.primary, + 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(), + padding: appBarOffset() ? const EdgeInsets.only(top: 60) : const EdgeInsets.only(), heightOffset: appBarOffset() ? 60 : 0, labelConstraints: const BoxConstraints(maxHeight: 28), scrollbarAnimationDuration: const Duration(milliseconds: 300), @@ -323,10 +306,7 @@ class ImmichAssetGridViewState extends ConsumerState { // 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, + (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, @@ -343,8 +323,7 @@ class ImmichAssetGridViewState extends ConsumerState { } else { ImmichToast.show( context: context, - msg: - "The date (${DateFormat.yMd().format(date)}) could not be found in the timeline.", + msg: "The date (${DateFormat.yMd().format(date)}) could not be found in the timeline.", gravity: ToastGravity.BOTTOM, toastType: ToastType.error, ); @@ -417,8 +396,7 @@ class ImmichAssetGridViewState extends ConsumerState { // on startup. if (_prevItemTime == null) { _prevItemTime = date; - } else if (_prevItemTime?.year != date.year || - _prevItemTime?.month != date.month) { + } else if (_prevItemTime?.year != date.year || _prevItemTime?.month != date.month) { _prevItemTime = date; final now = Timeline.now; @@ -511,12 +489,10 @@ class ImmichAssetGridViewState extends ConsumerState { final selectedAssets = {}; var currentSectionIndex = startSectionIndex; while (currentSectionIndex < endSectionIndex) { - final section = - widget.renderList.elements.elementAtOrNull(currentSectionIndex); + final section = widget.renderList.elements.elementAtOrNull(currentSectionIndex); if (section == null) continue; - final sectionAssets = - widget.renderList.loadAssets(section.offset, section.count); + final sectionAssets = widget.renderList.loadAssets(section.offset, section.count); if (currentSectionIndex == startSectionIndex) { selectedAssets.addAll( @@ -531,8 +507,7 @@ class ImmichAssetGridViewState extends ConsumerState { final section = widget.renderList.elements.elementAtOrNull(endSectionIndex); if (section != null) { - final sectionAssets = - widget.renderList.loadAssets(section.offset, section.count); + final sectionAssets = widget.renderList.loadAssets(section.offset, section.count); if (startSectionIndex == endSectionIndex) { selectedAssets.addAll( sectionAssets.slice(startSectionAssetIndex, endSectionAssetIndex + 1), @@ -562,8 +537,7 @@ class ImmichAssetGridViewState extends ConsumerState { /// "add to album" button. /// /// `_selectedAssets` includes `preselectedAssets` on initialization. - if (_selectedAssets.length > - (widget.preselectedAssets?.length ?? 0)) { + if (_selectedAssets.length > (widget.preselectedAssets?.length ?? 0)) { /// `_deselectAll` only deselects the selected assets, /// doesn't affect the preselected ones. _deselectAll(); @@ -585,8 +559,7 @@ class ImmichAssetGridViewState extends ConsumerState { ), child: _buildAssetGrid(), ), - if (widget.showMultiSelectIndicator && widget.selectionActive) - _buildMultiSelectIndicator(), + if (widget.showMultiSelectIndicator && widget.selectionActive) _buildMultiSelectIndicator(), ], ), ); @@ -671,26 +644,20 @@ class _Section extends StatelessWidget { ) { return LayoutBuilder( builder: (context, constraints) { - final width = constraints.maxWidth / assetsPerRow - - margin * (assetsPerRow - 1) / assetsPerRow; + 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); + 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.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), + assets: scrolling ? [] : renderList.loadAssets(section.offset, section.totalCount), allAssetsSelected: allAssetsSelected, selectAssets: selectAssets, deselectAssets: deselectAssets, @@ -699,9 +666,7 @@ class _Section extends StatelessWidget { scrolling ? _PlaceholderRow( key: ValueKey(i), - number: i + 1 == rows - ? section.count - i * assetsPerRow - : assetsPerRow, + number: i + 1 == rows ? section.count - i * assetsPerRow : assetsPerRow, width: width, height: width, margin: margin, @@ -747,9 +712,7 @@ class _MonthTitle extends StatelessWidget { @override Widget build(BuildContext context) { - final monthFormat = DateTime.now().year == date.year - ? DateFormat.MMMM() - : DateFormat.yMMMM(); + final monthFormat = DateTime.now().year == date.year ? DateFormat.MMMM() : DateFormat.yMMMM(); final String title = monthFormat.format(date); return Padding( key: Key("month-$title"), @@ -844,8 +807,7 @@ class _AssetRow extends StatelessWidget { final widthDistribution = List.filled(assets.length, 1.0); if (dynamicLayout) { - final aspectRatios = - assets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList(); + final aspectRatios = assets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList(); final meanAspectRatio = aspectRatios.sum / assets.length; // 1: mean width diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart index 98b1c6f601..a7c1290b30 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart @@ -65,11 +65,9 @@ class MultiselectGrid extends HookConsumerWidget { final bool unfavorite; final bool editEnabled; final Widget? emptyIndicator; - Widget buildDefaultLoadingIndicator() => - const Center(child: CircularProgressIndicator()); + Widget buildDefaultLoadingIndicator() => const Center(child: CircularProgressIndicator()); - Widget buildEmptyIndicator() => - emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()); + Widget buildEmptyIndicator() => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()); @override Widget build(BuildContext context, WidgetRef ref) { @@ -103,8 +101,7 @@ class MultiselectGrid extends HookConsumerWidget { ) { selectionEnabledHook.value = multiselect; selection.value = selectedAssets; - selectionAssetState.value = - AssetSelectionState.fromSelection(selectedAssets); + selectionAssetState.value = AssetSelectionState.fromSelection(selectedAssets); } errorBuilder(String? msg) => msg != null && msg.isNotEmpty @@ -120,16 +117,13 @@ class MultiselectGrid extends HookConsumerWidget { String? ownerErrorMessage, }) { final assets = selection.value; - return assets - .remoteOnly(errorCallback: errorBuilder(localErrorMessage)) - .ownedOnly( + return assets.remoteOnly(errorCallback: errorBuilder(localErrorMessage)).ownedOnly( currentUser, errorCallback: errorBuilder(ownerErrorMessage), ); } - Iterable remoteSelection({String? errorMessage}) => - selection.value.remoteOnly( + Iterable remoteSelection({String? errorMessage}) => selection.value.remoteOnly( errorCallback: errorBuilder(errorMessage), ); @@ -139,9 +133,7 @@ class MultiselectGrid extends HookConsumerWidget { // 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!); + final ids = remoteSelection(errorMessage: "home_page_share_err_local".tr()).map((e) => e.remoteId!); context.pushRoute(SharedLinkEditRoute(assetsList: ids.toList())); } processing.value = false; @@ -187,18 +179,14 @@ class MultiselectGrid extends HookConsumerWidget { errorCallback: errorBuilder('home_page_delete_err_partner'.tr()), ) .toList(); - final isDeleted = await ref - .read(assetProvider.notifier) - .deleteAssets(toDelete, force: force); + 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}"}), + ? 'assets_deleted_permanently'.tr(namedArgs: {'count': "${selection.value.length}"}) + : 'assets_trashed'.tr(namedArgs: {'count': "${selection.value.length}"}), gravity: ToastGravity.BOTTOM, ); selectionEnabledHook.value = false; @@ -213,26 +201,20 @@ class MultiselectGrid extends HookConsumerWidget { try { final localAssets = selection.value.where((a) => a.isLocal).toList(); - final toDelete = isMergedAsset - ? localAssets.where((e) => e.storage == AssetState.merged) - : localAssets; + 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()); + final isDeleted = await ref.read(assetProvider.notifier).deleteLocalAssets(toDelete.toList()); if (isDeleted) { - final deletedCount = - localAssets.where((e) => !isMergedAsset || e.isRemote).length; + final deletedCount = localAssets.where((e) => !isMergedAsset || e.isRemote).length; ImmichToast.show( context: context, - msg: 'assets_removed_permanently_from_device' - .tr(namedArgs: {'count': "$deletedCount"}), + msg: 'assets_removed_permanently_from_device'.tr(namedArgs: {'count': "$deletedCount"}), gravity: ToastGravity.BOTTOM, ); @@ -248,9 +230,7 @@ class MultiselectGrid extends HookConsumerWidget { try { final toDownload = selection.value.toList(); - final results = await ref - .read(downloadStateProvider.notifier) - .downloadAllAsset(toDownload); + final results = await ref.read(downloadStateProvider.notifier).downloadAllAsset(toDownload); final totalCount = toDownload.length; final successCount = results.where((e) => e).length; @@ -290,19 +270,16 @@ class MultiselectGrid extends HookConsumerWidget { ownerErrorMessage: 'home_page_delete_err_partner'.tr(), ).toList(); - final isDeleted = - await ref.read(assetProvider.notifier).deleteRemoteAssets( - toDelete, - shouldDeletePermanently: shouldDeletePermanently, - ); + 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}"}), + ? 'assets_deleted_permanently_from_server'.tr(namedArgs: {'count': "${toDelete.length}"}) + : 'assets_trashed_from_server'.tr(namedArgs: {'count': "${toDelete.length}"}), gravity: ToastGravity.BOTTOM, ); } @@ -379,9 +356,7 @@ class MultiselectGrid extends HookConsumerWidget { if (assets.isEmpty) { return; } - final result = await ref - .read(albumServiceProvider) - .createAlbumWithGeneratedName(assets); + final result = await ref.read(albumServiceProvider).createAlbumWithGeneratedName(assets); if (result != null) { ref.watch(albumProvider.notifier).refreshRemoteAlbums(); @@ -449,9 +424,7 @@ class MultiselectGrid extends HookConsumerWidget { ); if (remoteAssets.isNotEmpty) { final isInLockedView = ref.read(inLockedViewProvider); - final visibility = isInLockedView - ? AssetVisibilityEnum.timeline - : AssetVisibilityEnum.locked; + final visibility = isInLockedView ? AssetVisibilityEnum.timeline : AssetVisibilityEnum.locked; await handleSetAssetsVisibility( ref, @@ -489,8 +462,7 @@ class MultiselectGrid extends HookConsumerWidget { child: Stack( children: [ ref.watch(renderListProvider).when( - data: (data) => data.isEmpty && - (buildLoadingIndicator != null || topWidget == null) + data: (data) => data.isEmpty && (buildLoadingIndicator != null || topWidget == null) ? (buildLoadingIndicator ?? buildEmptyIndicator)() : ImmichAssetGrid( renderList: data, diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart b/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart index b17029f2af..10a541cec3 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart @@ -25,10 +25,8 @@ class MultiselectGridStatusIndicator extends HookConsumerWidget { ), ) : buildLoadingIndicator!(), - RenderListStatusEnum.empty => - emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()), - RenderListStatusEnum.error => - Center(child: const Text("error_loading_assets").tr()), + 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 index 25f65b448c..10815d00bb 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/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'; -import 'package:immich_mobile/utils/storage_indicator.dart'; -class ThumbnailImage extends ConsumerWidget { +class ThumbnailImage extends StatelessWidget { /// The asset to show the thumbnail image for final Asset asset; @@ -41,144 +40,9 @@ class ThumbnailImage extends ConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) { - final assetContainerColor = context.isDarkTheme - ? context.primaryColor.darken(amount: 0.6) - : context.primaryColor.lighten(amount: 0.8); - // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isFromDto = asset.id == noDbId; - - Widget buildSelectionIcon() { - if (isSelected) { - return Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: assetContainerColor, - ), - child: Icon( - Icons.check_circle_rounded, - color: context.primaryColor, - ), - ); - } else { - return const Icon( - Icons.circle_outlined, - color: Colors.white, - ); - } - } - - Widget buildVideoIcon() { - final minutes = asset.duration.inMinutes; - final durationString = asset.duration.toString(); - return Positioned( - top: 5, - right: 8, - child: Row( - children: [ - Text( - minutes > 59 - ? durationString.substring(0, 7) // h:mm:ss - : minutes > 0 - ? durationString.substring(2, 7) // mm:ss - : durationString.substring(3, 7), // m:ss - 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, - ), - ], - ), - ); - } - - Widget buildStackIcon() { - return Positioned( - top: !asset.isImage ? 28 : 5, - right: 8, - child: Row( - children: [ - if (asset.stackCount > 1) - Text( - "${asset.stackCount}", - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - if (asset.stackCount > 1) - const SizedBox( - width: 3, - ), - const Icon( - Icons.burst_mode_rounded, - color: Colors.white, - size: 18, - ), - ], - ), - ); - } - - Widget buildImage() { - final image = SizedBox.expand( - child: Hero( - tag: isFromDto - ? '${asset.remoteId}-$heroOffset' - : asset.id + heroOffset, - child: Stack( - children: [ - SizedBox.expand( - child: ImmichThumbnail( - asset: asset, - height: 250, - width: 250, - ), - ), - Container( - decoration: const 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 Container( - decoration: BoxDecoration( - color: canDeselect ? assetContainerColor : Colors.grey, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular(15.0), - ), - child: image, - ), - ); - } + Widget build(BuildContext context) { + final assetContainerColor = + context.isDarkTheme ? context.primaryColor.darken(amount: 0.6) : context.primaryColor.lighten(amount: 0.8); return Stack( children: [ @@ -187,32 +51,30 @@ class ThumbnailImage extends ConsumerWidget { curve: Curves.decelerate, decoration: BoxDecoration( border: multiselectEnabled && isSelected - ? Border.all( - color: canDeselect ? assetContainerColor : Colors.grey, - width: 8, - ) + ? 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: [ - buildImage(), - if (showStorageIndicator) - Positioned( - right: 8, - bottom: 5, - child: Icon( - storageIcon(asset), - color: Colors.white.withValues(alpha: .8), - size: 16, - shadows: [ - Shadow( - blurRadius: 5.0, - color: Colors.black.withValues(alpha: 0.6), - offset: const Offset(0.0, 0.0), - ), - ], - ), - ), + _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, @@ -223,20 +85,246 @@ class ThumbnailImage extends ConsumerWidget { size: 16, ), ), - if (!asset.isImage) buildVideoIcon(), - if (asset.stackCount > 0) buildStackIcon(), + if (asset.isVideo) _VideoIcon(duration: asset.duration), + if (asset.stackCount > 0) + _StackIcon( + isVideo: asset.isVideo, + stackCount: asset.stackCount, + ), ], ), ), if (multiselectEnabled) - Padding( - padding: const EdgeInsets.all(3.0), - child: Align( - alignment: Alignment.topLeft, - child: buildSelectionIcon(), - ), - ), + 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_viewer/advanced_bottom_sheet.dart b/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart index 1e6aba2bda..6fda361632 100644 --- a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart +++ b/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart @@ -35,10 +35,10 @@ class AdvancedBottomSheet extends HookConsumerWidget { const SizedBox(height: 32.0), Container( decoration: BoxDecoration( - color: context.isDarkTheme - ? Colors.grey[900] - : Colors.grey[200], - borderRadius: BorderRadius.circular(15.0), + color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[200], + borderRadius: const BorderRadius.all( + Radius.circular(15.0), + ), ), child: Padding( padding: const EdgeInsets.only( @@ -64,8 +64,7 @@ class AdvancedBottomSheet extends HookConsumerWidget { SnackBar( content: Text( "Copied to clipboard", - style: - context.textTheme.bodyLarge?.copyWith( + style: context.textTheme.bodyLarge?.copyWith( color: context.primaryColor, ), ), diff --git a/mobile/lib/widgets/asset_viewer/animated_play_pause.dart b/mobile/lib/widgets/asset_viewer/animated_play_pause.dart index 7935567b8c..dd0d95b93b 100644 --- a/mobile/lib/widgets/asset_viewer/animated_play_pause.dart +++ b/mobile/lib/widgets/asset_viewer/animated_play_pause.dart @@ -17,8 +17,7 @@ class AnimatedPlayPause extends StatefulWidget { State createState() => AnimatedPlayPauseState(); } -class AnimatedPlayPauseState extends State - with SingleTickerProviderStateMixin { +class AnimatedPlayPauseState extends State with SingleTickerProviderStateMixin { late final animationController = AnimationController( vsync: this, value: widget.playing ? 1 : 0, diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index 59d97bf0c7..66392b4bb4 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -52,28 +52,21 @@ class BottomGalleryBar extends ConsumerWidget { if (asset == null) { return const SizedBox(); } - final isOwner = - asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? ''); + 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)) - : []; + 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 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); + ref.read(assetStackStateProvider(stackId).notifier).removeChild(stackIndex.value - 1); } } @@ -89,8 +82,7 @@ class BottomGalleryBar extends ConsumerWidget { // `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) { + if (totalAssets.value == 1 || assetIndex.value == totalAssets.value - 1) { // Handle only one asset context.maybePop(); } @@ -98,9 +90,7 @@ class BottomGalleryBar extends ConsumerWidget { totalAssets.value -= 1; } if (isDeleted) { - ref - .read(currentAssetProvider.notifier) - .set(renderList.loadAsset(assetIndex.value)); + ref.read(currentAssetProvider.notifier).set(renderList.loadAsset(assetIndex.value)); } return isDeleted; } @@ -144,9 +134,7 @@ class BottomGalleryBar extends ConsumerWidget { return; } - await ref - .read(stackServiceProvider) - .deleteStack(asset.stackId!, stackItems); + await ref.read(stackServiceProvider).deleteStack(asset.stackId!, stackItems); } void showStackActionItems() { @@ -240,8 +228,7 @@ class BottomGalleryBar extends ConsumerWidget { handleRemoveFromAlbum() async { final album = ref.read(currentAlbumProvider); - final bool isSuccess = album != null && - await ref.read(albumProvider.notifier).removeAsset(album, [asset]); + final bool isSuccess = album != null && await ref.read(albumProvider.notifier).removeAsset(album, [asset]); if (isSuccess) { // Workaround for asset remaining in the gallery @@ -373,9 +360,7 @@ class BottomGalleryBar extends ConsumerWidget { unselectedItemColor: Colors.white, showSelectedLabels: true, showUnselectedLabels: true, - items: albumActions - .map((e) => e.keys.first) - .toList(growable: false), + 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/cast_dialog.dart b/mobile/lib/widgets/asset_viewer/cast_dialog.dart index 9043ea4bea..a0373bcb6c 100644 --- a/mobile/lib/widgets/asset_viewer/cast_dialog.dart +++ b/mobile/lib/widgets/asset_viewer/cast_dialog.dart @@ -49,10 +49,8 @@ class CastDialog extends ConsumerWidget { } final devices = snapshot.data!; - final connected = - devices.where((d) => isCurrentDevice(d.$1)).toList(); - final others = - devices.where((d) => !isCurrentDevice(d.$1)).toList(); + final connected = devices.where((d) => isCurrentDevice(d.$1)).toList(); + final others = devices.where((d) => !isCurrentDevice(d.$1)).toList(); final List sectionedList = []; @@ -85,25 +83,18 @@ class CastDialog extends ConsumerWidget { ).tr(), ); } else { - final (deviceName, type, deviceObj) = - item as (String, CastDestinationType, dynamic); + final (deviceName, type, deviceObj) = item as (String, CastDestinationType, dynamic); return ListTile( title: Text( deviceName, style: TextStyle( - color: isCurrentDevice(deviceName) - ? context.colorScheme.primary - : null, + color: isCurrentDevice(deviceName) ? context.colorScheme.primary : null, ), ), leading: Icon( - type == CastDestinationType.googleCast - ? Icons.cast - : Icons.cast_connected, - color: isCurrentDevice(deviceName) - ? context.colorScheme.primary - : null, + type == CastDestinationType.googleCast ? Icons.cast : Icons.cast_connected, + color: isCurrentDevice(deviceName) ? context.colorScheme.primary : null, ), trailing: isCurrentDevice(deviceName) ? Icon(Icons.check, color: context.colorScheme.primary) @@ -120,9 +111,7 @@ class CastDialog extends ConsumerWidget { } if (!isCurrentDevice(deviceName)) { - ref - .read(castProvider.notifier) - .connect(type, deviceObj); + ref.read(castProvider.notifier).connect(type, deviceObj); } }, ); diff --git a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart index d64e507170..d70761f37d 100644 --- a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart +++ b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart @@ -24,8 +24,7 @@ class CustomVideoPlayerControls extends HookConsumerWidget { currentAssetProvider.select((asset) => asset != null && asset.isVideo), ); final showControls = ref.watch(showControlsProvider); - final VideoPlaybackState state = - ref.watch(videoPlaybackValueProvider.select((value) => value.state)); + final VideoPlaybackState state = ref.watch(videoPlaybackValueProvider.select((value) => value.state)); final cast = ref.watch(castProvider); @@ -39,15 +38,12 @@ class CustomVideoPlayerControls extends HookConsumerWidget { final state = ref.read(videoPlaybackValueProvider).state; // Do not hide on paused - if (state != VideoPlaybackState.paused && - state != VideoPlaybackState.completed && - assetIsVideo) { + if (state != VideoPlaybackState.paused && state != VideoPlaybackState.completed && assetIsVideo) { ref.read(showControlsProvider.notifier).show = false; } }, ); - final showBuffering = - state == VideoPlaybackState.buffering && !cast.isCasting; + final showBuffering = state == VideoPlaybackState.buffering && !cast.isCasting; /// Shows the controls and starts the timer to hide them void showControlsAndStartHideTimer() { @@ -56,8 +52,7 @@ class CustomVideoPlayerControls extends HookConsumerWidget { } // When we change position, show or hide timer - ref.listen(videoPlayerControlsProvider.select((v) => v.position), - (previous, next) { + ref.listen(videoPlayerControlsProvider.select((v) => v.position), (previous, next) { showControlsAndStartHideTimer(); }); @@ -76,7 +71,7 @@ class CustomVideoPlayerControls extends HookConsumerWidget { if (asset == null) { return; } - ref.read(castProvider.notifier).loadMedia(asset, true); + ref.read(castProvider.notifier).loadMediaOld(asset, true); } return; } @@ -105,14 +100,13 @@ class CustomVideoPlayerControls extends HookConsumerWidget { ) else GestureDetector( - onTap: () => - ref.read(showControlsProvider.notifier).show = false, + onTap: () => ref.read(showControlsProvider.notifier).show = false, child: CenterPlayButton( backgroundColor: Colors.black54, iconColor: Colors.white, isFinished: state == VideoPlaybackState.completed, - isPlaying: state == VideoPlaybackState.playing || - (cast.isCasting && cast.castState == CastState.playing), + isPlaying: + state == VideoPlaybackState.playing || (cast.isCasting && cast.castState == CastState.playing), show: assetIsVideo && showControls, onPressed: togglePlay, ), diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart index aec18c6a16..bd859b8ced 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart @@ -24,10 +24,7 @@ class CameraInfo extends StatelessWidget { "${exifInfo.make} ${exifInfo.model}", style: context.textTheme.labelLarge, ), - subtitle: exifInfo.f != null || - exifInfo.exposureSeconds != null || - exifInfo.mm != null || - exifInfo.iso != null + 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, diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart index 8ad2cdc687..97c9477c97 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart @@ -24,9 +24,7 @@ class DetailPanel extends HookConsumerWidget { child: Column( children: [ AssetDateTime(asset: asset), - asset.isRemote - ? DescriptionInput(asset: asset) - : const SizedBox.shrink(), + 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/exif_map.dart b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart index f3f72dfd87..7b6325cf2c 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart @@ -9,11 +9,13 @@ import 'package:url_launcher/url_launcher.dart'; class ExifMap extends StatelessWidget { final ExifInfo exifInfo; final String? markerId; + final MapCreatedCallback? onMapCreated; const ExifMap({ super.key, required this.exifInfo, this.markerId = 'marker', + this.onMapCreated, }); @override @@ -82,6 +84,7 @@ class ExifMap extends StatelessWidget { debugPrint('Opening Map Uri: $uri'); launchUrl(uri); }, + onCreated: onMapCreated, ); }, ); diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart index 4af9846cf6..486918c436 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart @@ -17,11 +17,8 @@ class FileInfo extends StatelessWidget { 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 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; diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart index cbb003bd72..a97a04a453 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart @@ -18,12 +18,8 @@ class PeopleInfo extends ConsumerWidget { @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); + final peopleProvider = ref.watch(assetPeopleNotifierProvider(asset).notifier); + final people = ref.watch(assetPeopleNotifierProvider(asset)).value?.where((p) => !p.isHidden); showPersonNameEditModel( String personId, @@ -46,8 +42,7 @@ class PeopleInfo extends ConsumerWidget { (p) => SearchCuratedContent( id: p.id, label: p.name, - subtitle: p.birthDate != null && - p.birthDate!.isBefore(asset.fileCreatedAt) + subtitle: p.birthDate != null && p.birthDate!.isBefore(asset.fileCreatedAt) ? _formatAge(p.birthDate!, asset.fileCreatedAt) : null, ), @@ -56,9 +51,7 @@ class PeopleInfo extends ConsumerWidget { []; return AnimatedCrossFade( - crossFadeState: (people?.isEmpty ?? true) - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, + crossFadeState: (people?.isEmpty ?? true) ? CrossFadeState.showFirst : CrossFadeState.showSecond, duration: const Duration(milliseconds: 200), firstChild: Container(), secondChild: Padding( @@ -109,22 +102,18 @@ class PeopleInfo extends ConsumerWidget { int ageInMonths = _calculateAgeInMonths(birthDate, referenceDate); if (ageInMonths <= 11) { - return "exif_bottom_sheet_person_age_months" - .tr(namedArgs: {'months': ageInMonths.toString()}); + return "exif_bottom_sheet_person_age_months".tr(namedArgs: {'months': ageInMonths.toString()}); } else if (ageInMonths > 12 && ageInMonths <= 23) { - return "exif_bottom_sheet_person_age_year_months" - .tr(namedArgs: {'months': (ageInMonths - 12).toString()}); + return "exif_bottom_sheet_person_age_year_months".tr(namedArgs: {'months': (ageInMonths - 12).toString()}); } else { - return "exif_bottom_sheet_person_age_years" - .tr(namedArgs: {'years': ageInYears.toString()}); + return "exif_bottom_sheet_person_age_years".tr(namedArgs: {'years': ageInYears.toString()}); } } int _calculateAge(DateTime birthDate, DateTime referenceDate) { int age = referenceDate.year - birthDate.year; if (referenceDate.month < birthDate.month || - (referenceDate.month == birthDate.month && - referenceDate.day < birthDate.day)) { + (referenceDate.month == birthDate.month && referenceDate.day < birthDate.day)) { age--; } return age; diff --git a/mobile/lib/widgets/asset_viewer/formatted_duration.dart b/mobile/lib/widgets/asset_viewer/formatted_duration.dart index a34aab7d12..d18dc92575 100644 --- a/mobile/lib/widgets/asset_viewer/formatted_duration.dart +++ b/mobile/lib/widgets/asset_viewer/formatted_duration.dart @@ -1,15 +1,5 @@ import 'package:flutter/material.dart'; - -@pragma('vm:prefer-inline') -String _formatDuration(Duration position) { - final seconds = position.inSeconds.remainder(60).toString().padLeft(2, "0"); - final minutes = position.inMinutes.remainder(60).toString().padLeft(2, "0"); - if (position.inHours == 0) { - return "$minutes:$seconds"; - } - final hours = position.inHours.toString().padLeft(2, '0'); - return "$hours:$minutes:$seconds"; -} +import 'package:immich_mobile/extensions/duration_extensions.dart'; class FormattedDuration extends StatelessWidget { final Duration data; @@ -20,7 +10,7 @@ class FormattedDuration extends StatelessWidget { return SizedBox( width: data.inHours > 0 ? 70 : 60, // use a fixed width to prevent jitter child: Text( - _formatDuration(data), + data.format(), style: const TextStyle( fontSize: 14.0, color: Colors.white, diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart index 4ef55f4f76..1c24412259 100644 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart @@ -34,17 +34,12 @@ class GalleryAppBar extends ConsumerWidget { return const SizedBox(); } final album = ref.watch(currentAlbumProvider); - final isOwner = - asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? ''); + 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); + final isPartner = ref.watch(partnerSharedWithProvider).map((e) => fastHash(e.id)).contains(asset.ownerId); - toggleFavorite(Asset asset) => - ref.read(assetProvider.notifier).toggleFavorite([asset]); + toggleFavorite(Asset asset) => ref.read(assetProvider.notifier).toggleFavorite([asset]); handleActivities() { if (album != null && album.shared && album.remoteId != null) { @@ -53,8 +48,7 @@ class GalleryAppBar extends ConsumerWidget { } handleRestore(Asset asset) async { - final result = - await ref.read(trashProvider.notifier).restoreAssets([asset]); + final result = await ref.read(trashProvider.notifier).restoreAssets([asset]); if (result && context.mounted) { ImmichToast.show( @@ -71,9 +65,7 @@ class GalleryAppBar extends ConsumerWidget { builder: (BuildContext _) { return UploadDialog( onUpload: () { - ref - .read(manualUploadProvider.notifier) - .uploadAssets(context, [asset]); + ref.read(manualUploadProvider.notifier).uploadAssets(context, [asset]); }, ); }, @@ -83,8 +75,10 @@ class GalleryAppBar extends ConsumerWidget { addToAlbum(Asset addToAlbumAsset) { showModalBottomSheet( elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15.0), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), ), context: context, builder: (BuildContext _) { @@ -102,8 +96,7 @@ class GalleryAppBar extends ConsumerWidget { handleLocateAsset() async { // Go back to the gallery await context.maybePop(); - await context - .navigateTo(const TabControllerRoute(children: [PhotosRoute()])); + 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); diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart index a868aff617..1d04115b7c 100644 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart @@ -49,12 +49,9 @@ class TopControlAppBar extends HookConsumerWidget { 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 websocketConnected = ref.watch(websocketProvider.select((c) => c.isConnected)); - final comments = album != null && - album.remoteId != null && - asset.remoteId != null + final comments = album != null && album.remoteId != null && asset.remoteId != null ? ref.watch(activityStatisticsProvider(album.remoteId!, asset.remoteId)) : 0; @@ -204,24 +201,14 @@ class TopControlAppBar extends HookConsumerWidget { shape: const Border(), actions: [ if (asset.isRemote && isOwner) buildFavoriteButton(a), - if (isOwner && - !isInHomePage && - !(isInTrash ?? false) && - !isInLockedView) - buildLocateButton(), + 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.isRemote && (isOwner || isPartner) && !asset.isTrashed && !isInLockedView) buildAddToAlbumButton(), + if (isCasting || (asset.isRemote && websocketConnected)) buildCastButton(), if (asset.isTrashed) buildRestoreButton(), - if (album != null && album.shared && !isInLockedView) - buildActivitiesButton(), + if (album != null && album.shared && !isInLockedView) buildActivitiesButton(), buildMoreInfoButton(), ], ); diff --git a/mobile/lib/widgets/asset_viewer/video_position.dart b/mobile/lib/widgets/asset_viewer/video_position.dart index 0e90669fe3..2bd2eb80bf 100644 --- a/mobile/lib/widgets/asset_viewer/video_position.dart +++ b/mobile/lib/widgets/asset_viewer/video_position.dart @@ -54,8 +54,7 @@ class VideoPosition extends HookConsumerWidget { activeColor: Colors.white, inactiveColor: whiteOpacity75, onChangeStart: (value) { - final state = - ref.read(videoPlaybackValueProvider).state; + final state = ref.read(videoPlaybackValueProvider).state; wasPlaying.value = state != VideoPlaybackState.paused; ref.read(videoPlayerControlsProvider.notifier).pause(); }, @@ -68,19 +67,14 @@ class VideoPosition extends HookConsumerWidget { final seekToDuration = (duration * (value / 100.0)); if (isCasting) { - ref - .read(castProvider.notifier) - .seekTo(seekToDuration); + ref.read(castProvider.notifier).seekTo(seekToDuration); return; } - ref - .read(videoPlayerControlsProvider.notifier) - .position = seekToDuration.inSeconds.toDouble(); + ref.read(videoPlayerControlsProvider.notifier).position = seekToDuration.inSeconds.toDouble(); // This immediately updates the slider position without waiting for the video to update - ref.read(videoPlaybackValueProvider.notifier).position = - seekToDuration; + ref.read(videoPlaybackValueProvider.notifier).position = seekToDuration; }, ), ), diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart index 526d2199be..696168a384 100644 --- a/mobile/lib/widgets/backup/album_info_card.dart +++ b/mobile/lib/widgets/backup/album_info_card.dart @@ -8,8 +8,8 @@ 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/routing/router.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -23,13 +23,9 @@ class AlbumInfoCard extends HookConsumerWidget { @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 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; @@ -37,16 +33,16 @@ class AlbumInfoCard extends HookConsumerWidget { 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); + 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: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + ), label: Text( "album_info_card_backup_album_included", style: TextStyle( @@ -60,7 +56,9 @@ class AlbumInfoCard extends HookConsumerWidget { } else if (isExcluded) { return Chip( visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + ), label: Text( "album_info_card_backup_album_excluded", style: TextStyle( @@ -125,11 +123,11 @@ class AlbumInfoCard extends HookConsumerWidget { clipBehavior: Clip.hardEdge, margin: const EdgeInsets.all(1), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), // if you need this + 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), + color: isDarkTheme ? const Color.fromARGB(255, 37, 35, 35) : const Color(0xFFC9C9C9), width: 1, ), ), @@ -184,8 +182,7 @@ class AlbumInfoCard extends HookConsumerWidget { Padding( padding: const EdgeInsets.only(top: 2.0), child: Text( - album.assetCount.toString() + - (album.isAll ? " (${'all'.tr()})" : ""), + album.assetCount.toString() + (album.isAll ? " (${'all'.tr()})" : ""), style: TextStyle( fontSize: 12, color: Colors.grey[600], diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index a263c004bd..7558b909bb 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -19,23 +19,15 @@ class AlbumInfoListTile extends HookConsumerWidget { @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 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); + 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); + return context.isDarkTheme ? Colors.red[300]?.withAlpha(150) : Colors.red[100]?.withAlpha(150); } else { return Colors.transparent; } diff --git a/mobile/lib/widgets/backup/asset_info_table.dart b/mobile/lib/widgets/backup/asset_info_table.dart index 98bcc2b3da..a87832d3f1 100644 --- a/mobile/lib/widgets/backup/asset_info_table.dart +++ b/mobile/lib/widgets/backup/asset_info_table.dart @@ -82,9 +82,7 @@ class BackupAssetInfoTable extends ConsumerWidget { ), ).tr( namedArgs: { - 'date': isUploadInProgress - ? _getAssetCreationDate(asset) - : "-", + 'date': isUploadInProgress ? _getAssetCreationDate(asset) : "-", }, ), ), diff --git a/mobile/lib/widgets/backup/backup_info_card.dart b/mobile/lib/widgets/backup/backup_info_card.dart index 58fc89cb65..54551da35a 100644 --- a/mobile/lib/widgets/backup/backup_info_card.dart +++ b/mobile/lib/widgets/backup/backup_info_card.dart @@ -18,7 +18,9 @@ class BackupInfoCard extends StatelessWidget { Widget build(BuildContext context) { return Card( shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), // if you need this + borderRadius: const BorderRadius.all( + Radius.circular(20), // if you need this + ), side: BorderSide( color: context.colorScheme.outlineVariant, width: 1, diff --git a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart new file mode 100644 index 0000000000..25d5bfef28 --- /dev/null +++ b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart @@ -0,0 +1,115 @@ +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/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class DriftAlbumInfoListTile extends HookConsumerWidget { + final LocalAlbum album; + + const DriftAlbumInfoListTile({super.key, required this.album}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final bool isSelected = album.backupSelection == BackupSelection.selected; + final bool isExcluded = album.backupSelection == BackupSelection.excluded; + + final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); + + buildTileColor() { + if (isSelected) { + return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25); + } 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) { + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + } else { + 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(backupAlbumProvider.notifier).excludeAlbum(album); + } + }, + child: ListTile( + tileColor: buildTileColor(), + contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + onTap: () { + ref.read(hapticFeedbackProvider.notifier).selectionClick(); + if (isSelected) { + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + } else { + ref.read(backupAlbumProvider.notifier).selectAlbum(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(LocalTimelineRoute(album: album)); + }, + icon: Icon( + Icons.image_outlined, + color: context.primaryColor, + size: 24, + ), + splashRadius: 25, + ), + ), + ); + } +} diff --git a/mobile/lib/widgets/backup/error_chip.dart b/mobile/lib/widgets/backup/error_chip.dart index 4df3e50f64..3c743a0903 100644 --- a/mobile/lib/widgets/backup/error_chip.dart +++ b/mobile/lib/widgets/backup/error_chip.dart @@ -11,8 +11,7 @@ class BackupErrorChip extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final hasErrors = - ref.watch(errorBackupListProvider.select((value) => value.isNotEmpty)); + final hasErrors = ref.watch(errorBackupListProvider.select((value) => value.isNotEmpty)); if (!hasErrors) { return const SizedBox(); } diff --git a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart b/mobile/lib/widgets/backup/icloud_download_progress_bar.dart index c61fb1a0d1..7d292112ea 100644 --- a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart +++ b/mobile/lib/widgets/backup/icloud_download_progress_bar.dart @@ -17,20 +17,17 @@ class IcloudDownloadProgressBar extends ConsumerWidget { final isIcloudAsset = isManualUpload ? ref.watch( - manualUploadProvider - .select((value) => value.currentUploadAsset.isIcloudAsset), + manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset), ) : ref.watch( - backupProvider - .select((value) => value.currentUploadAsset.isIcloudAsset), + backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset), ); if (!isIcloudAsset) { return const SizedBox(); } - final iCloudDownloadProgress = ref - .watch(backupProvider.select((value) => value.iCloudDownloadProgress)); + final iCloudDownloadProgress = ref.watch(backupProvider.select((value) => value.iCloudDownloadProgress)); return Padding( padding: const EdgeInsets.only(top: 8.0), diff --git a/mobile/lib/widgets/backup/ios_debug_info_tile.dart b/mobile/lib/widgets/backup/ios_debug_info_tile.dart index de80b3bfd1..0f96e4cef6 100644 --- a/mobile/lib/widgets/backup/ios_debug_info_tile.dart +++ b/mobile/lib/widgets/backup/ios_debug_info_tile.dart @@ -24,8 +24,7 @@ class IosDebugInfoTile extends HookConsumerWidget { 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}); + title = 'ios_debug_info_processes_queued'.t(context: context, args: {'count': processes}); } final df = DateFormat.yMd().add_jm(); @@ -33,14 +32,11 @@ class IosDebugInfoTile extends HookConsumerWidget { 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)}); + 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)}); + subtitle = 'ios_debug_info_processing_ran_at'.t(context: context, args: {'dateTime': df.format(processing)}); } else { - final fetchOrProcessing = - fetch!.isAfter(processing!) ? fetch : processing; + final fetchOrProcessing = fetch!.isAfter(processing!) ? fetch : processing; subtitle = 'ios_debug_info_last_sync_at'.t( context: context, args: {'dateTime': df.format(fetchOrProcessing)}, diff --git a/mobile/lib/widgets/backup/upload_progress_bar.dart b/mobile/lib/widgets/backup/upload_progress_bar.dart index 9281914d9c..b11a8562b9 100644 --- a/mobile/lib/widgets/backup/upload_progress_bar.dart +++ b/mobile/lib/widgets/backup/upload_progress_bar.dart @@ -18,12 +18,10 @@ class BackupUploadProgressBar extends ConsumerWidget { final isIcloudAsset = isManualUpload ? ref.watch( - manualUploadProvider - .select((value) => value.currentUploadAsset.isIcloudAsset), + manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset), ) : ref.watch( - backupProvider - .select((value) => value.currentUploadAsset.isIcloudAsset), + backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset), ); final uploadProgress = isManualUpload 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 cc14ffa5fe..e0fa03ba01 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 @@ -17,6 +17,8 @@ import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_server_info.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; +import 'package:immich_mobile/widgets/common/immich_logo.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; class ImmichAppBarDialog extends HookConsumerWidget { @@ -56,9 +58,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ), Center( child: Image.asset( - context.isDarkTheme - ? 'assets/immich-text-dark.png' - : 'assets/immich-text-light.png', + context.isDarkTheme ? 'assets/immich-text-dark.png' : 'assets/immich-text-light.png', height: 16, ), ), @@ -129,10 +129,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ok: "yes", onOk: () async { isLoggingOut.value = true; - await ref - .read(authProvider.notifier) - .logout() - .whenComplete(() => isLoggingOut.value = false); + await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false); ref.read(manualUploadProvider.notifier).cancelBackup(); ref.read(backupProvider.notifier).cancelBackup(); @@ -194,14 +191,12 @@ class ImmichAppBarDialog extends HookConsumerWidget { child: LinearProgressIndicator( minHeight: 10.0, value: percentage, - borderRadius: - const BorderRadius.all(Radius.circular(10.0)), + borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), ), Padding( padding: const EdgeInsets.only(top: 12.0), - child: - const Text('backup_controller_page_storage_format').tr( + child: const Text('backup_controller_page_storage_format').tr( namedArgs: { 'used': usedDiskSpace, 'total': totalDiskSpace, @@ -255,6 +250,28 @@ class ImmichAppBarDialog extends HookConsumerWidget { style: context.textTheme.bodySmall, ).tr(), ), + const SizedBox( + width: 20, + child: Text( + "â€ĸ", + textAlign: TextAlign.center, + ), + ), + InkWell( + onTap: () async { + context.pop(); + final packageInfo = await PackageInfo.fromPlatform(); + showLicensePage( + context: context, + applicationIcon: const Padding( + padding: EdgeInsetsGeometry.symmetric(vertical: 10), + child: ImmichLogo(size: 40), + ), + applicationVersion: packageInfo.version, + ); + }, + child: Text("licenses", style: context.textTheme.bodySmall).tr(), + ), ], ), ); @@ -274,8 +291,10 @@ class ImmichAppBarDialog extends HookConsumerWidget { right: horizontalPadding, bottom: isHorizontal ? 20 : 100, ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(20), + ), ), child: SizedBox( child: SingleChildScrollView( 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 93cdcd0e6f..080c17e951 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 @@ -17,8 +17,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(authProvider); - final uploadProfileImageStatus = - ref.watch(uploadProfileImageProvider).status; + final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status; final user = ref.watch(currentUserProvider); buildUserProfileImage() { @@ -55,12 +54,10 @@ class AppBarProfileInfoBox extends HookConsumerWidget { ); if (image != null) { - var success = - await ref.watch(uploadProfileImageProvider.notifier).upload(image); + var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image); if (success) { - final profileImagePath = - ref.read(uploadProfileImageProvider).profileImagePath; + final profileImagePath = ref.read(uploadProfileImageProvider).profileImagePath; ref.watch(authProvider.notifier).updateUserProfileImagePath( profileImagePath, ); @@ -96,8 +93,8 @@ class AppBarProfileInfoBox extends HookConsumerWidget { child: Material( color: context.colorScheme.surfaceContainerHighest, elevation: 3, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50.0), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(50.0)), ), child: Padding( padding: const EdgeInsets.all(5.0), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 0f8ae8b8e1..6b990c1c08 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -1,10 +1,10 @@ +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/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/utils/url_helper.dart'; @@ -173,11 +173,10 @@ class AppBarServerInfo extends HookConsumerWidget { verticalOffset: 0, decoration: BoxDecoration( color: context.primaryColor.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(10), + borderRadius: const BorderRadius.all(Radius.circular(10)), ), textStyle: TextStyle( - color: - context.isDarkTheme ? Colors.black : Colors.white, + color: context.isDarkTheme ? Colors.black : Colors.white, fontWeight: FontWeight.bold, ), message: getServerUrl() ?? '--', diff --git a/mobile/lib/widgets/common/date_time_picker.dart b/mobile/lib/widgets/common/date_time_picker.dart index 22384ebe8e..92d0b48684 100644 --- a/mobile/lib/widgets/common/date_time_picker.dart +++ b/mobile/lib/widgets/common/date_time_picker.dart @@ -73,10 +73,7 @@ class _DateTimePicker extends HookWidget { // returns a list of location along with it's offset in duration List<_TimeZoneOffset> getAllTimeZones() { - return tz.timeZoneDatabase.locations.values - .map(_TimeZoneOffset.fromLocation) - .sorted() - .toList(); + return tz.timeZoneDatabase.locations.values.map(_TimeZoneOffset.fromLocation).sorted().toList(); } @override @@ -125,11 +122,9 @@ class _DateTimePicker extends HookWidget { } void popWithDateTime() { - final formattedDateTime = - DateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date.value); - final dtWithOffset = formattedDateTime + - Duration(milliseconds: tzOffset.value.offsetInMilliseconds) - .formatAsOffset(); + final formattedDateTime = DateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date.value); + final dtWithOffset = + formattedDateTime + Duration(milliseconds: tzOffset.value.offsetInMilliseconds).formatAsOffset(); context.pop(dtWithOffset); } @@ -172,11 +167,15 @@ class _DateTimePicker extends HookWidget { ListTile( tileColor: context.colorScheme.surfaceContainerHighest, shape: ShapeBorder.lerp( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), ), - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), ), 1, ), @@ -241,19 +240,15 @@ class _TimeZoneOffset implements Comparable<_TimeZoneOffset> { } @override - String toString() => - '_TimeZoneOffset(display: $display, location: $location)'; + String toString() => '_TimeZoneOffset(display: $display, location: $location)'; @override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is _TimeZoneOffset && - other.display == display && - other.offsetInMilliseconds == offsetInMilliseconds; + return other is _TimeZoneOffset && other.display == display && other.offsetInMilliseconds == offsetInMilliseconds; } @override - int get hashCode => - display.hashCode ^ offsetInMilliseconds.hashCode ^ location.hashCode; + int get hashCode => display.hashCode ^ offsetInMilliseconds.hashCode ^ location.hashCode; } diff --git a/mobile/lib/widgets/common/drag_sheet.dart b/mobile/lib/widgets/common/drag_sheet.dart index a98e000e51..0e4651d819 100644 --- a/mobile/lib/widgets/common/drag_sheet.dart +++ b/mobile/lib/widgets/common/drag_sheet.dart @@ -33,8 +33,7 @@ class ControlBoxButton extends StatelessWidget { @override Widget build(BuildContext context) { - final minWidth = - context.isMobile ? MediaQuery.sizeOf(context).width / 4.5 : 75.0; + final minWidth = context.isMobile ? MediaQuery.sizeOf(context).width / 4.5 : 75.0; return MaterialButton( padding: const EdgeInsets.all(10), diff --git a/mobile/lib/widgets/common/dropdown_search_menu.dart b/mobile/lib/widgets/common/dropdown_search_menu.dart index cf954a8977..cde60b2afb 100644 --- a/mobile/lib/widgets/common/dropdown_search_menu.dart +++ b/mobile/lib/widgets/common/dropdown_search_menu.dart @@ -30,8 +30,7 @@ class DropdownSearchMenu extends HookWidget { @override Widget build(BuildContext context) { final selectedItem = useState?>( - dropdownMenuEntries - .firstWhereOrNull((item) => item.value == initialSelection), + dropdownMenuEntries.firstWhereOrNull((item) => item.value == initialSelection), ); final showTimeZoneDropdown = useState(false); @@ -77,10 +76,7 @@ class DropdownSearchMenu extends HookWidget { displayStringForOption: (option) => option.label, optionsBuilder: (textEditingValue) { return dropdownMenuEntries.where( - (item) => item.label - .toLowerCase() - .trim() - .contains(textEditingValue.text.toLowerCase().trim()), + (item) => item.label.toLowerCase().trim().contains(textEditingValue.text.toLowerCase().trim()), ); }, onSelected: (option) { @@ -127,9 +123,7 @@ class DropdownSearchMenu extends HookWidget { onTap: () => onSelected(option), child: Builder( builder: (BuildContext context) { - final bool highlight = - AutocompleteHighlightedOption.of(context) == - index; + final bool highlight = AutocompleteHighlightedOption.of(context) == index; if (highlight) { SchedulerBinding.instance.addPostFrameCallback( (Duration timeStamp) { @@ -142,12 +136,7 @@ class DropdownSearchMenu extends HookWidget { ); } return Container( - color: highlight - ? Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.12) - : null, + color: highlight ? Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.12) : null, padding: const EdgeInsets.all(16.0), child: Text( option.label, diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index c3bb75afd6..0d77e02aa5 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -27,8 +27,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { @override Widget build(BuildContext context, WidgetRef ref) { final BackUpState backupState = ref.watch(backupProvider); - final bool isEnableAutoBackup = - backupState.backgroundBackup || backupState.autoBackup; + final bool isEnableAutoBackup = backupState.backgroundBackup || backupState.autoBackup; final ServerInfo serverInfoState = ref.watch(serverInfoProvider); final user = ref.watch(currentUserProvider); final isDarkTheme = context.isDarkTheme; @@ -42,7 +41,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog(), ), - borderRadius: BorderRadius.circular(12), + borderRadius: const BorderRadius.all(Radius.circular(12)), child: Badge( label: Container( decoration: BoxDecoration( @@ -57,9 +56,8 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { ), backgroundColor: Colors.transparent, alignment: Alignment.bottomRight, - isLabelVisible: serverInfoState.isVersionMismatch || - ((user?.isAdmin ?? false) && - serverInfoState.isNewReleaseAvailable), + isLabelVisible: + serverInfoState.isVersionMismatch || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable), offset: const Offset(-2, -12), child: user == null ? const Icon( @@ -92,8 +90,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { semanticsLabel: 'backup_controller_page_backup'.tr(), ), ); - } else if (backupState.backupProgress != - BackUpProgressEnum.inBackground && + } else if (backupState.backupProgress != BackUpProgressEnum.inBackground && backupState.backupProgress != BackUpProgressEnum.manualInProgress) { return Icon( Icons.check_outlined, @@ -120,7 +117,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { return InkWell( onTap: () => context.pushRoute(const BackupControllerRoute()), - borderRadius: BorderRadius.circular(12), + borderRadius: const BorderRadius.all(Radius.circular(12)), child: Badge( label: Container( width: widgetSize / 2, diff --git a/mobile/lib/widgets/common/immich_image.dart b/mobile/lib/widgets/common/immich_image.dart index cbad9037c0..f51748edd6 100644 --- a/mobile/lib/widgets/common/immich_image.dart +++ b/mobile/lib/widgets/common/immich_image.dart @@ -59,8 +59,7 @@ class ImmichImage extends StatelessWidget { // 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); + !asset.isRemote || asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false); @override Widget build(BuildContext context) { diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart new file mode 100644 index 0000000000..9f84b7cd7f --- /dev/null +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -0,0 +1,373 @@ +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_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/models/server_info/server_info.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/sync_status.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/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 ImmichSliverAppBar extends ConsumerWidget { + final List? actions; + final bool showUploadButton; + final bool floating; + final bool pinned; + final bool snap; + final Widget? title; + final double? expandedHeight; + + const ImmichSliverAppBar({ + super.key, + this.actions, + this.showUploadButton = true, + this.floating = true, + this.pinned = false, + this.snap = true, + this.title, + this.expandedHeight, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); + final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled)); + + return SliverAnimatedOpacity( + duration: Durations.medium1, + opacity: isMultiSelectEnabled ? 0 : 1, + sliver: SliverAppBar( + floating: floating, + pinned: pinned, + snap: snap, + expandedHeight: expandedHeight, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + automaticallyImplyLeading: false, + centerTitle: false, + title: title ?? const _ImmichLogoWithText(), + actions: [ + 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, + ), + ), + ), + const _SyncStatusIndicator(), + if (actions != null) + ...actions!.map( + (action) => Padding( + padding: const EdgeInsets.only(right: 16), + child: action, + ), + ), + if (kDebugMode || kProfileMode) + IconButton( + icon: const Icon(Icons.science_rounded), + onPressed: () => context.pushRoute(const FeatInDevRoute()), + ), + if (showUploadButton) + const Padding( + padding: EdgeInsets.only(right: 20), + child: _BackupIndicator(), + ), + const Padding( + padding: EdgeInsets.only(right: 20), + child: _ProfileIndicator(), + ), + ], + ), + ); + } +} + +class _ImmichLogoWithText extends StatelessWidget { + const _ImmichLogoWithText(); + + @override + Widget build(BuildContext context) { + return Builder( + builder: (BuildContext context) { + return Row( + children: [ + Builder( + builder: (context) { + return Badge( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + backgroundColor: context.primaryColor, + alignment: Alignment.centerRight, + offset: const Offset(16, -8), + label: Text( + 'β', + style: TextStyle( + fontSize: 11, + color: context.colorScheme.onPrimary, + fontWeight: FontWeight.bold, + fontFamily: 'OverpassMono', + height: 1.2, + ), + ), + child: Padding( + padding: const EdgeInsets.only(top: 3.0), + child: SvgPicture.asset( + context.isDarkTheme + ? 'assets/immich-logo-inline-dark.svg' + : 'assets/immich-logo-inline-light.svg', + height: 40, + ), + ), + ); + }, + ), + ], + ); + }, + ); + } +} + +class _ProfileIndicator extends ConsumerWidget { + const _ProfileIndicator(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ServerInfo serverInfoState = ref.watch(serverInfoProvider); + final user = ref.watch(currentUserProvider); + const widgetSize = 30.0; + + 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: + serverInfoState.isVersionMismatch || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable), + 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( + radius: 17, + size: 31, + user: user, + ), + ), + ), + ); + } +} + +class _BackupIndicator extends ConsumerWidget { + const _BackupIndicator(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + const widgetSize = 30.0; + final indicatorIcon = _getBackupBadgeIcon(context, ref); + final badgeBackground = context.colorScheme.surfaceContainer; + + return InkWell( + onTap: () => context.pushRoute(const DriftBackupRoute()), + 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, + ), + ), + ); + } + + Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) { + final BackUpState backupState = ref.watch(backupProvider); + final bool isEnableAutoBackup = backupState.backgroundBackup || backupState.autoBackup; + final isDarkTheme = context.isDarkTheme; + 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(), + ); + } + + return null; + } +} + +class _SyncStatusIndicator extends ConsumerStatefulWidget { + const _SyncStatusIndicator(); + + @override + ConsumerState<_SyncStatusIndicator> createState() => _SyncStatusIndicatorState(); +} + +class _SyncStatusIndicatorState extends ConsumerState<_SyncStatusIndicator> with TickerProviderStateMixin { + late AnimationController _rotationController; + late AnimationController _dismissalController; + late Animation _rotationAnimation; + late Animation _dismissalAnimation; + + @override + void initState() { + super.initState(); + _rotationController = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + ); + _dismissalController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _rotationAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(_rotationController); + _dismissalAnimation = Tween( + begin: 1.0, + end: 0.0, + ).animate( + CurvedAnimation( + parent: _dismissalController, + curve: Curves.easeOutQuart, + ), + ); + } + + @override + void dispose() { + _rotationController.dispose(); + _dismissalController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final syncStatus = ref.watch(syncStatusProvider); + final isSyncing = syncStatus.isRemoteSyncing; + + // Control animations based on sync status + if (isSyncing) { + if (!_rotationController.isAnimating) { + _rotationController.repeat(); + } + _dismissalController.reset(); + } else { + _rotationController.stop(); + if (_dismissalController.status == AnimationStatus.dismissed) { + _dismissalController.forward(); + } + } + + // Don't show anything if not syncing and dismissal animation is complete + if (!isSyncing && _dismissalController.status == AnimationStatus.completed) { + return const SizedBox.shrink(); + } + + return AnimatedBuilder( + animation: Listenable.merge([_rotationAnimation, _dismissalAnimation]), + builder: (context, child) { + return Padding( + padding: EdgeInsets.only(right: isSyncing ? 16 : 0), + child: Transform.scale( + scale: isSyncing ? 1.0 : _dismissalAnimation.value, + child: Opacity( + opacity: isSyncing ? 1.0 : _dismissalAnimation.value, + child: Transform.rotate( + angle: _rotationAnimation.value * 2 * 3.14159 * -1, // Rotate counter-clockwise + child: Icon( + Icons.sync, + size: 24, + color: context.primaryColor, + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/widgets/common/immich_thumbnail.dart b/mobile/lib/widgets/common/immich_thumbnail.dart index 0918348f4c..5e3bb610d8 100644 --- a/mobile/lib/widgets/common/immich_thumbnail.dart +++ b/mobile/lib/widgets/common/immich_thumbnail.dart @@ -93,8 +93,7 @@ class ImmichThumbnail extends HookConsumerWidget { customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) { thumbnailProviderInstance.evict(); - final originalErrorWidgetBuilder = - blurHashErrorBuilder(blurhash, fit: fit); + final originalErrorWidgetBuilder = blurHashErrorBuilder(blurhash, fit: fit); return originalErrorWidgetBuilder(ctx, error, stackTrace); } diff --git a/mobile/lib/widgets/common/immich_title_text.dart b/mobile/lib/widgets/common/immich_title_text.dart index 711d0bf396..456ecdc9cf 100644 --- a/mobile/lib/widgets/common/immich_title_text.dart +++ b/mobile/lib/widgets/common/immich_title_text.dart @@ -15,9 +15,7 @@ class ImmichTitleText extends StatelessWidget { Widget build(BuildContext context) { return Image( image: AssetImage( - context.isDarkTheme - ? 'assets/immich-text-dark.png' - : 'assets/immich-text-light.png', + context.isDarkTheme ? 'assets/immich-text-dark.png' : 'assets/immich-text-light.png', ), width: fontSize * 4, filterQuality: FilterQuality.high, diff --git a/mobile/lib/widgets/common/local_album_sliver_app_bar.dart b/mobile/lib/widgets/common/local_album_sliver_app_bar.dart new file mode 100644 index 0000000000..4880865e66 --- /dev/null +++ b/mobile/lib/widgets/common/local_album_sliver_app_bar.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; + +class LocalAlbumsSliverAppBar extends StatelessWidget { + const LocalAlbumsSliverAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return SliverAppBar( + floating: true, + pinned: true, + snap: false, + backgroundColor: context.colorScheme.surfaceContainer, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + automaticallyImplyLeading: true, + centerTitle: true, + title: Text( + "on_this_device".t(context: context), + ), + ); + } +} diff --git a/mobile/lib/widgets/common/location_picker.dart b/mobile/lib/widgets/common/location_picker.dart index 7bfdf296bb..81f8440836 100644 --- a/mobile/lib/widgets/common/location_picker.dart +++ b/mobile/lib/widgets/common/location_picker.dart @@ -56,8 +56,7 @@ class _LocationPicker extends HookWidget { ? _MapPicker( key: ValueKey(latlng), latlng: latlng, - onModeSwitch: () => - pickerMode.value = _LocationPickerMode.manual, + onModeSwitch: () => pickerMode.value = _LocationPickerMode.manual, onMapTap: onMapTap, ) : _ManualPicker( @@ -141,8 +140,7 @@ class _ManualPickerInput extends HookWidget { errorText: isValid.value ? null : errorText.tr(), ), onEditingComplete: onEditingComplete, - keyboardType: - const TextInputType.numberWithOptions(decimal: true, signed: true), + keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), inputFormatters: [LengthLimitingTextInputFormatter(8)], onTapOutside: (_) => focusNode.unfocus(), ); diff --git a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart new file mode 100644 index 0000000000..2130a07866 --- /dev/null +++ b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart @@ -0,0 +1,550 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; + +class MesmerizingSliverAppBar extends ConsumerStatefulWidget { + const MesmerizingSliverAppBar({ + super.key, + required this.title, + this.icon = Icons.camera, + }); + + final String title; + final IconData icon; + @override + ConsumerState createState() => _MesmerizingSliverAppBarState(); +} + +class _MesmerizingSliverAppBarState extends ConsumerState { + double _scrollProgress = 0.0; + + double _calculateScrollProgress(FlexibleSpaceBarSettings? settings) { + if (settings?.maxExtent == null || settings?.minExtent == null) { + return 1.0; + } + + final deltaExtent = settings!.maxExtent - settings.minExtent; + if (deltaExtent <= 0.0) { + return 1.0; + } + + return (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent).clamp(0.0, 1.0); + } + + @override + Widget build(BuildContext context) { + final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled)); + + return isMultiSelectEnabled + ? SliverToBoxAdapter( + child: switch (_scrollProgress) { + < 0.8 => const SizedBox(height: 120), + _ => const SizedBox(height: 352), + }, + ) + : SliverAppBar( + expandedHeight: 300.0, + floating: false, + pinned: true, + snap: false, + elevation: 0, + leading: IconButton( + icon: Icon( + Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back, + color: Color.lerp( + Colors.white, + context.primaryColor, + _scrollProgress, + ), + shadows: [ + _scrollProgress < 0.95 + ? Shadow( + offset: const Offset(0, 2), + blurRadius: 5, + color: Colors.black.withValues(alpha: 0.5), + ) + : const Shadow( + offset: Offset(0, 2), + blurRadius: 0, + color: Colors.transparent, + ), + ], + ), + onPressed: () { + context.pop(); + }, + ), + flexibleSpace: Builder( + builder: (context) { + final settings = context.dependOnInheritedWidgetOfExactType(); + final scrollProgress = _calculateScrollProgress(settings); + + // Update scroll progress for the leading button + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && _scrollProgress != scrollProgress) { + setState(() { + _scrollProgress = scrollProgress; + }); + } + }); + + return FlexibleSpaceBar( + centerTitle: true, + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: scrollProgress > 0.95 + ? Text( + widget.title, + style: TextStyle( + color: context.primaryColor, + fontWeight: FontWeight.w600, + fontSize: 18, + ), + ) + : null, + ), + background: _ExpandedBackground( + scrollProgress: scrollProgress, + title: widget.title, + icon: widget.icon, + ), + ); + }, + ), + ); + } +} + +class _ExpandedBackground extends ConsumerStatefulWidget { + final double scrollProgress; + final String title; + final IconData icon; + + const _ExpandedBackground({ + required this.scrollProgress, + required this.title, + required this.icon, + }); + + @override + ConsumerState<_ExpandedBackground> createState() => _ExpandedBackgroundState(); +} + +class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with SingleTickerProviderStateMixin { + late AnimationController _slideController; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + + _slideController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _slideAnimation = Tween( + begin: const Offset(0, 1.5), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _slideController, + curve: Curves.easeOutCubic, + ), + ); + + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) { + _slideController.forward(); + } + }); + } + + @override + void dispose() { + _slideController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final timelineService = ref.watch(timelineServiceProvider); + + return Stack( + fit: StackFit.expand, + children: [ + Transform.translate( + offset: Offset(0, widget.scrollProgress * 50), + child: Transform.scale( + scale: 1.4 - (widget.scrollProgress * 0.2), + child: _RandomAssetBackground( + timelineService: timelineService, + icon: widget.icon, + ), + ), + ), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.transparent, + Colors.black.withValues( + alpha: 0.6 + (widget.scrollProgress * 0.2), + ), + ], + stops: const [0.0, 0.65, 1.0], + ), + ), + ), + Positioned( + bottom: 16, + left: 16, + right: 16, + child: SlideTransition( + position: _slideAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: double.infinity, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Text( + widget.title, + maxLines: 1, + style: const TextStyle( + color: Colors.white, + fontSize: 36, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + shadows: [ + Shadow( + offset: Offset(0, 2), + blurRadius: 12, + color: Colors.black45, + ), + ], + ), + ), + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + child: const _ItemCountText(), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _ItemCountText extends ConsumerStatefulWidget { + const _ItemCountText(); + + @override + ConsumerState<_ItemCountText> createState() => _ItemCountTextState(); +} + +class _ItemCountTextState extends ConsumerState<_ItemCountText> { + StreamSubscription? _reloadSubscription; + + @override + void initState() { + super.initState(); + _reloadSubscription = EventStream.shared.listen((_) => setState(() {})); + } + + @override + void dispose() { + _reloadSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final assetCount = ref.watch( + timelineServiceProvider.select((s) => s.totalAssets), + ); + + return Text( + 'items_count'.t( + context: context, + args: {"count": assetCount}, + ), + style: context.textTheme.labelLarge?.copyWith( + // letterSpacing: 0.2, + fontWeight: FontWeight.bold, + color: Colors.white, + shadows: [ + const Shadow( + offset: Offset(0, 1), + blurRadius: 6, + color: Colors.black45, + ), + ], + ), + ); + } +} + +class _RandomAssetBackground extends StatefulWidget { + final TimelineService timelineService; + final IconData icon; + + const _RandomAssetBackground({ + required this.timelineService, + required this.icon, + }); + + @override + State<_RandomAssetBackground> createState() => _RandomAssetBackgroundState(); +} + +class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with TickerProviderStateMixin { + late AnimationController _zoomController; + late AnimationController _crossFadeController; + late Animation _zoomAnimation; + late Animation _panAnimation; + late Animation _crossFadeAnimation; + BaseAsset? _currentAsset; + BaseAsset? _nextAsset; + bool _isZoomingIn = true; + + @override + void initState() { + super.initState(); + + _zoomController = AnimationController( + duration: const Duration(seconds: 12), + vsync: this, + ); + + _crossFadeController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + + _zoomAnimation = Tween( + begin: 1.0, + end: 1.2, + ).animate( + CurvedAnimation( + parent: _zoomController, + curve: Curves.easeInOut, + ), + ); + + _panAnimation = Tween( + begin: Offset.zero, + end: const Offset(0.5, -0.5), + ).animate( + CurvedAnimation( + parent: _zoomController, + curve: Curves.easeInOut, + ), + ); + + _crossFadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate( + CurvedAnimation( + parent: _crossFadeController, + curve: Curves.easeInOutCubic, + ), + ); + + Future.delayed( + Durations.medium1, + () => _loadFirstAsset(), + ); + } + + @override + void dispose() { + _zoomController.dispose(); + _crossFadeController.dispose(); + super.dispose(); + } + + void _startAnimationCycle() { + if (_isZoomingIn) { + _zoomController.forward().then((_) { + _loadNextAsset(); + }); + } else { + _zoomController.reverse().then((_) { + _loadNextAsset(); + }); + } + } + + Future _loadFirstAsset() async { + if (!mounted) { + return; + } + + if (widget.timelineService.totalAssets == 0) { + setState(() { + _currentAsset = null; + }); + + return; + } + + setState(() { + _currentAsset = widget.timelineService.getRandomAsset(); + }); + + await _crossFadeController.forward(); + + if (_zoomController.status == AnimationStatus.dismissed) { + if (_isZoomingIn) { + _zoomController.reset(); + } else { + _zoomController.value = 1.0; + } + _startAnimationCycle(); + } + } + + Future _loadNextAsset() async { + if (!mounted) { + return; + } + + try { + if (widget.timelineService.totalAssets > 1) { + // Load next asset while keeping current one visible + final nextAsset = widget.timelineService.getRandomAsset(); + + setState(() { + _nextAsset = nextAsset; + }); + + await _crossFadeController.reverse(); + setState(() { + _currentAsset = _nextAsset; + _nextAsset = null; + }); + + _crossFadeController.value = 1.0; + + _isZoomingIn = !_isZoomingIn; + + _startAnimationCycle(); + } + } catch (e) { + _zoomController.reset(); + _startAnimationCycle(); + } + } + + @override + Widget build(BuildContext context) { + if (widget.timelineService.totalAssets == 0) { + return const SizedBox.shrink(); + } + + return AnimatedBuilder( + animation: Listenable.merge( + [_zoomAnimation, _panAnimation, _crossFadeAnimation], + ), + builder: (context, child) { + return Transform.scale( + scale: _zoomAnimation.value, + filterQuality: Platform.isAndroid ? FilterQuality.low : null, + child: Transform.translate( + offset: _panAnimation.value, + filterQuality: Platform.isAndroid ? FilterQuality.low : null, + child: Stack( + fit: StackFit.expand, + children: [ + // Current image + if (_currentAsset != null) + Opacity( + opacity: _crossFadeAnimation.value, + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Image( + alignment: Alignment.topRight, + image: getFullImageProvider(_currentAsset!), + fit: BoxFit.cover, + frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded || frame != null) { + return child; + } + return Container(); + }, + errorBuilder: (context, error, stackTrace) { + return SizedBox( + width: double.infinity, + height: double.infinity, + child: Icon( + Icons.error_outline_rounded, + size: 24, + color: Colors.red[300], + ), + ); + }, + ), + ), + ), + + if (_nextAsset != null) + Opacity( + opacity: 1.0 - _crossFadeAnimation.value, + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Image( + alignment: Alignment.topRight, + image: getFullImageProvider(_nextAsset!), + fit: BoxFit.cover, + frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded || frame != null) { + return child; + } + return const SizedBox.shrink(); + }, + errorBuilder: (context, error, stackTrace) { + return SizedBox( + width: double.infinity, + height: double.infinity, + child: Icon( + Icons.error_outline_rounded, + size: 24, + color: Colors.red[300], + ), + ); + }, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart new file mode 100644 index 0000000000..6f26c87da7 --- /dev/null +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -0,0 +1,675 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart'; + +class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget { + const RemoteAlbumSliverAppBar({ + super.key, + this.icon = Icons.camera, + this.onShowOptions, + this.onToggleAlbumOrder, + this.onEditTitle, + }); + + final IconData icon; + final void Function()? onShowOptions; + final void Function()? onToggleAlbumOrder; + final void Function()? onEditTitle; + + @override + ConsumerState createState() => _MesmerizingSliverAppBarState(); +} + +class _MesmerizingSliverAppBarState extends ConsumerState { + double _scrollProgress = 0.0; + + double _calculateScrollProgress(FlexibleSpaceBarSettings? settings) { + if (settings?.maxExtent == null || settings?.minExtent == null) { + return 1.0; + } + + final deltaExtent = settings!.maxExtent - settings.minExtent; + if (deltaExtent <= 0.0) { + return 1.0; + } + + return (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent).clamp(0.0, 1.0); + } + + @override + Widget build(BuildContext context) { + final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled)); + + final currentAlbum = ref.watch(currentRemoteAlbumProvider); + if (currentAlbum == null) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + + Color? actionIconColor = Color.lerp( + Colors.white, + context.primaryColor, + _scrollProgress, + ); + + List actionIconShadows = [ + if (_scrollProgress < 0.95) + Shadow( + offset: const Offset(0, 2), + blurRadius: 5, + color: Colors.black.withValues(alpha: 0.5), + ) + else + const Shadow( + offset: Offset(0, 2), + blurRadius: 0, + color: Colors.transparent, + ), + ]; + + return isMultiSelectEnabled + ? SliverToBoxAdapter( + child: switch (_scrollProgress) { + < 0.8 => const SizedBox(height: 120), + _ => const SizedBox(height: 452), + }, + ) + : SliverAppBar( + expandedHeight: 400.0, + floating: false, + pinned: true, + snap: false, + elevation: 0, + leading: IconButton( + icon: Icon( + Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back, + color: actionIconColor, + shadows: actionIconShadows, + ), + onPressed: () { + ref.read(remoteAlbumProvider.notifier).refresh(); + context.pop(); + }, + ), + actions: [ + if (widget.onToggleAlbumOrder != null) + IconButton( + icon: Icon( + Icons.swap_vert_rounded, + color: actionIconColor, + shadows: actionIconShadows, + ), + onPressed: widget.onToggleAlbumOrder, + ), + if (widget.onShowOptions != null) + IconButton( + icon: Icon( + Icons.more_vert, + color: actionIconColor, + shadows: actionIconShadows, + ), + onPressed: widget.onShowOptions, + ), + ], + flexibleSpace: Builder( + builder: (context) { + final settings = context.dependOnInheritedWidgetOfExactType(); + final scrollProgress = _calculateScrollProgress(settings); + + // Update scroll progress for the leading button + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && _scrollProgress != scrollProgress) { + setState(() { + _scrollProgress = scrollProgress; + }); + } + }); + + return FlexibleSpaceBar( + centerTitle: true, + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: scrollProgress > 0.95 + ? Text( + currentAlbum.name, + style: TextStyle( + color: context.primaryColor, + fontWeight: FontWeight.w600, + fontSize: 18, + ), + ) + : null, + ), + background: _ExpandedBackground( + scrollProgress: scrollProgress, + icon: widget.icon, + onEditTitle: widget.onEditTitle, + ), + ); + }, + ), + ); + } +} + +class _ExpandedBackground extends ConsumerStatefulWidget { + final double scrollProgress; + final IconData icon; + final void Function()? onEditTitle; + + const _ExpandedBackground({ + required this.scrollProgress, + required this.icon, + this.onEditTitle, + }); + + @override + ConsumerState<_ExpandedBackground> createState() => _ExpandedBackgroundState(); +} + +class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with SingleTickerProviderStateMixin { + late AnimationController _slideController; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + + _slideController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _slideAnimation = Tween( + begin: const Offset(0, 1.5), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _slideController, + curve: Curves.easeOutCubic, + ), + ); + + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) { + _slideController.forward(); + } + }); + } + + @override + void dispose() { + _slideController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final timelineService = ref.watch(timelineServiceProvider); + final currentAlbum = ref.watch(currentRemoteAlbumProvider); + + if (currentAlbum == null) { + return const SizedBox.shrink(); + } + + final dateRange = ref.watch( + remoteAlbumDateRangeProvider(currentAlbum.id), + ); + return Stack( + fit: StackFit.expand, + children: [ + Transform.translate( + offset: Offset(0, widget.scrollProgress * 50), + child: Transform.scale( + scale: 1.4 - (widget.scrollProgress * 0.2), + child: _RandomAssetBackground( + timelineService: timelineService, + icon: widget.icon, + ), + ), + ), + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: widget.scrollProgress * 2.0, + sigmaY: widget.scrollProgress * 2.0, + ), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withValues(alpha: 0.05), + Colors.transparent, + Colors.black.withValues(alpha: 0.3), + Colors.black.withValues( + alpha: 0.6 + (widget.scrollProgress * 0.25), + ), + ], + stops: const [0.0, 0.15, 0.55, 1.0], + ), + ), + ), + ), + ), + Positioned( + bottom: 16, + left: 16, + right: 16, + child: SlideTransition( + position: _slideAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + if (dateRange.hasValue) + Text( + DateRangeFormatting.formatDateRange( + dateRange.value!.$1.toLocal(), + dateRange.value!.$2.toLocal(), + context.locale, + ), + style: const TextStyle( + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(0, 2), + blurRadius: 12, + color: Colors.black87, + ), + ], + ), + ), + const Text( + " â€ĸ ", + style: TextStyle( + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(0, 2), + blurRadius: 12, + color: Colors.black87, + ), + ], + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + child: const _ItemCountText(), + ), + ], + ), + GestureDetector( + onTap: widget.onEditTitle, + child: SizedBox( + width: double.infinity, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Text( + currentAlbum.name, + maxLines: 1, + style: const TextStyle( + color: Colors.white, + fontSize: 36, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + shadows: [ + Shadow( + offset: Offset(0, 2), + blurRadius: 12, + color: Colors.black54, + ), + ], + ), + ), + ), + ), + ), + if (currentAlbum.description.isNotEmpty) + GestureDetector( + onTap: widget.onEditTitle, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 80, + ), + child: SingleChildScrollView( + child: Text( + currentAlbum.description, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + shadows: [ + Shadow( + offset: Offset(0, 2), + blurRadius: 8, + color: Colors.black54, + ), + ], + ), + ), + ), + ), + ), + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: RemoteAlbumSharedUserIcons(), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _ItemCountText extends ConsumerStatefulWidget { + const _ItemCountText(); + + @override + ConsumerState<_ItemCountText> createState() => _ItemCountTextState(); +} + +class _ItemCountTextState extends ConsumerState<_ItemCountText> { + StreamSubscription? _reloadSubscription; + + @override + void initState() { + super.initState(); + _reloadSubscription = EventStream.shared.listen((_) => setState(() {})); + } + + @override + void dispose() { + _reloadSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final assetCount = ref.watch( + timelineServiceProvider.select((s) => s.totalAssets), + ); + + return Text( + 'items_count'.t( + context: context, + args: {"count": assetCount}, + ), + style: context.textTheme.labelLarge?.copyWith( + color: Colors.white, + shadows: [ + const Shadow( + offset: Offset(0, 2), + blurRadius: 12, + color: Colors.black87, + ), + ], + ), + ); + } +} + +class _RandomAssetBackground extends StatefulWidget { + final TimelineService timelineService; + final IconData icon; + + const _RandomAssetBackground({ + required this.timelineService, + required this.icon, + }); + + @override + State<_RandomAssetBackground> createState() => _RandomAssetBackgroundState(); +} + +class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with TickerProviderStateMixin { + late AnimationController _zoomController; + late AnimationController _crossFadeController; + late Animation _zoomAnimation; + late Animation _panAnimation; + late Animation _crossFadeAnimation; + BaseAsset? _currentAsset; + BaseAsset? _nextAsset; + bool _isZoomingIn = true; + + @override + void initState() { + super.initState(); + + _zoomController = AnimationController( + duration: const Duration(seconds: 12), + vsync: this, + ); + + _crossFadeController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + + _zoomAnimation = Tween( + begin: 1.0, + end: 1.2, + ).animate( + CurvedAnimation( + parent: _zoomController, + curve: Curves.easeInOut, + ), + ); + + _panAnimation = Tween( + begin: Offset.zero, + end: const Offset(0.5, -0.5), + ).animate( + CurvedAnimation( + parent: _zoomController, + curve: Curves.easeInOut, + ), + ); + + _crossFadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate( + CurvedAnimation( + parent: _crossFadeController, + curve: Curves.easeInOutCubic, + ), + ); + + Future.delayed( + Durations.medium1, + () => _loadFirstAsset(), + ); + } + + @override + void dispose() { + _zoomController.dispose(); + _crossFadeController.dispose(); + super.dispose(); + } + + void _startAnimationCycle() { + if (_isZoomingIn) { + _zoomController.forward().then((_) { + _loadNextAsset(); + }); + } else { + _zoomController.reverse().then((_) { + _loadNextAsset(); + }); + } + } + + Future _loadFirstAsset() async { + if (!mounted) { + return; + } + + if (widget.timelineService.totalAssets == 0) { + setState(() { + _currentAsset = null; + }); + + return; + } + + setState(() { + _currentAsset = widget.timelineService.getRandomAsset(); + }); + + await _crossFadeController.forward(); + + if (_zoomController.status == AnimationStatus.dismissed) { + if (_isZoomingIn) { + _zoomController.reset(); + } else { + _zoomController.value = 1.0; + } + _startAnimationCycle(); + } + } + + Future _loadNextAsset() async { + if (!mounted) { + return; + } + + try { + if (widget.timelineService.totalAssets > 1) { + // Load next asset while keeping current one visible + final nextAsset = widget.timelineService.getRandomAsset(); + + setState(() { + _nextAsset = nextAsset; + }); + + await _crossFadeController.reverse(); + setState(() { + _currentAsset = _nextAsset; + _nextAsset = null; + }); + + _crossFadeController.value = 1.0; + + _isZoomingIn = !_isZoomingIn; + + _startAnimationCycle(); + } + } catch (e) { + _zoomController.reset(); + _startAnimationCycle(); + } + } + + @override + Widget build(BuildContext context) { + if (widget.timelineService.totalAssets == 0) { + return const SizedBox.shrink(); + } + + return AnimatedBuilder( + animation: Listenable.merge( + [_zoomAnimation, _panAnimation, _crossFadeAnimation], + ), + builder: (context, child) { + return Transform.scale( + scale: _zoomAnimation.value, + filterQuality: Platform.isAndroid ? FilterQuality.low : null, + child: Transform.translate( + offset: _panAnimation.value, + filterQuality: Platform.isAndroid ? FilterQuality.low : null, + child: Stack( + fit: StackFit.expand, + children: [ + // Current image + if (_currentAsset != null) + Opacity( + opacity: _crossFadeAnimation.value, + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Image( + alignment: Alignment.topRight, + image: getFullImageProvider(_currentAsset!), + fit: BoxFit.cover, + frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded || frame != null) { + return child; + } + return Container(); + }, + errorBuilder: (context, error, stackTrace) { + return SizedBox( + width: double.infinity, + height: double.infinity, + child: Icon( + Icons.error_outline_rounded, + size: 24, + color: Colors.red[300], + ), + ); + }, + ), + ), + ), + + if (_nextAsset != null) + Opacity( + opacity: 1.0 - _crossFadeAnimation.value, + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Image( + alignment: Alignment.topRight, + image: getFullImageProvider(_nextAsset!), + fit: BoxFit.cover, + frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded || frame != null) { + return child; + } + return const SizedBox.shrink(); + }, + errorBuilder: (context, error, stackTrace) { + return SizedBox( + width: double.infinity, + height: double.infinity, + child: Icon( + Icons.error_outline_rounded, + size: 24, + color: Colors.red[300], + ), + ); + }, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/widgets/common/scaffold_error_body.dart b/mobile/lib/widgets/common/scaffold_error_body.dart index 5011d229e7..f1d7685f73 100644 --- a/mobile/lib/widgets/common/scaffold_error_body.dart +++ b/mobile/lib/widgets/common/scaffold_error_body.dart @@ -27,8 +27,7 @@ class ScaffoldErrorBody extends StatelessWidget { child: Icon( Icons.error_outline, size: 100, - color: - context.themeData.iconTheme.color?.withValues(alpha: 0.5), + color: context.themeData.iconTheme.color?.withValues(alpha: 0.5), ), ), ), diff --git a/mobile/lib/widgets/common/search_field.dart b/mobile/lib/widgets/common/search_field.dart index 5d71082b24..97ac75a63b 100644 --- a/mobile/lib/widgets/common/search_field.dart +++ b/mobile/lib/widgets/common/search_field.dart @@ -47,25 +47,33 @@ class SearchField extends StatelessWidget { color: context.themeData.colorScheme.onSurfaceSecondary, ), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(25), + borderRadius: const BorderRadius.all( + Radius.circular(25), + ), borderSide: BorderSide( color: context.colorScheme.surfaceDim, ), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(25), + borderRadius: const BorderRadius.all( + Radius.circular(25), + ), borderSide: BorderSide( color: context.colorScheme.surfaceContainer, ), ), disabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(25), + borderRadius: const BorderRadius.all( + Radius.circular(25), + ), borderSide: BorderSide( color: context.colorScheme.surfaceDim, ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(25), + borderRadius: const BorderRadius.all( + Radius.circular(25), + ), borderSide: BorderSide( color: context.colorScheme.primary.withAlpha(100), ), diff --git a/mobile/lib/widgets/common/selection_sliver_app_bar.dart b/mobile/lib/widgets/common/selection_sliver_app_bar.dart new file mode 100644 index 0000000000..4a6dbbf385 --- /dev/null +++ b/mobile/lib/widgets/common/selection_sliver_app_bar.dart @@ -0,0 +1,76 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; + +class SelectionSliverAppBar extends ConsumerStatefulWidget { + const SelectionSliverAppBar({ + super.key, + }); + + @override + ConsumerState createState() => _SelectionSliverAppBarState(); +} + +class _SelectionSliverAppBarState extends ConsumerState { + @override + Widget build(BuildContext context) { + final selection = ref.watch( + multiSelectProvider.select((s) => s.selectedAssets), + ); + + final toExclude = ref.watch( + multiSelectProvider.select((s) => s.lockedSelectionAssets), + ); + + final filteredAssets = selection.where((asset) { + return !toExclude.contains(asset); + }).toSet(); + + onDone(Set selected) { + ref.read(multiSelectProvider.notifier).reset(); + context.maybePop>(selected); + } + + return SliverAppBar( + floating: true, + pinned: true, + snap: false, + backgroundColor: context.colorScheme.surfaceContainer, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + automaticallyImplyLeading: false, + leading: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () { + ref.read(multiSelectProvider.notifier).reset(); + context.pop>(null); + }, + ), + centerTitle: true, + title: Text( + "Select {count}".t( + context: context, + args: { + 'count': filteredAssets.length.toString(), + }, + ), + ), + actions: [ + TextButton( + onPressed: () => onDone(filteredAssets), + child: Text( + 'done'.t(context: context), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.primary, + ), + ), + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/common/thumbhash_placeholder.dart b/mobile/lib/widgets/common/thumbhash_placeholder.dart index aa320f4230..f73aa869f7 100644 --- a/mobile/lib/widgets/common/thumbhash_placeholder.dart +++ b/mobile/lib/widgets/common/thumbhash_placeholder.dart @@ -13,8 +13,7 @@ OctoSet blurHashOrPlaceholder( }) { return OctoSet( placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), - errorBuilder: - blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage), + errorBuilder: blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage), ); } diff --git a/mobile/lib/widgets/common/user_avatar.dart b/mobile/lib/widgets/common/user_avatar.dart index a5a6fa2bdd..ff0e39f371 100644 --- a/mobile/lib/widgets/common/user_avatar.dart +++ b/mobile/lib/widgets/common/user_avatar.dart @@ -7,8 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/services/api.service.dart'; Widget userAvatar(BuildContext context, UserDto u, {double? radius}) { - final url = - "${Store.get(StoreKey.serverEndpoint)}/users/${u.id}/profile-image"; + final url = "${Store.get(StoreKey.serverEndpoint)}/users/${u.id}/profile-image"; final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : ""; return CircleAvatar( radius: radius, diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart index 8866cb01b0..c1d34c4baa 100644 --- a/mobile/lib/widgets/common/user_circle_avatar.dart +++ b/mobile/lib/widgets/common/user_circle_avatar.dart @@ -5,9 +5,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/widgets/common/transparent_image.dart'; @@ -16,17 +14,19 @@ class UserCircleAvatar extends ConsumerWidget { final UserDto user; double radius; double size; + bool hasBorder; UserCircleAvatar({ super.key, this.radius = 22, this.size = 44, + this.hasBorder = false, required this.user, }); @override Widget build(BuildContext context, WidgetRef ref) { - bool isDarkTheme = context.themeData.brightness == Brightness.dark; + final userAvatarColor = user.avatarColor.toColor(); final profileImageUrl = '${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}'; @@ -34,31 +34,43 @@ class UserCircleAvatar extends ConsumerWidget { style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12, - color: isDarkTheme && user.avatarColor == AvatarColor.primary - ? Colors.black - : Colors.white, + color: userAvatarColor.computeLuminance() > 0.5 ? Colors.black : Colors.white, ), child: Text(user.name[0].toUpperCase()), ); - return CircleAvatar( - backgroundColor: user.avatarColor.toColor(), - radius: radius, - child: user.profileImagePath == null - ? textIcon - : ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(50)), - child: CachedNetworkImage( - fit: BoxFit.cover, - cacheKey: user.profileImagePath, - width: size, - height: size, - placeholder: (_, __) => Image.memory(kTransparentImage), - imageUrl: profileImageUrl, - httpHeaders: ApiService.getRequestHeaders(), - fadeInDuration: const Duration(milliseconds: 300), - errorWidget: (context, error, stackTrace) => textIcon, - ), - ), + return Tooltip( + message: user.name, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: hasBorder + ? Border.all( + color: Colors.grey[500]!, + width: 1, + ) + : null, + ), + child: CircleAvatar( + backgroundColor: userAvatarColor, + radius: radius, + child: user.profileImagePath == null + ? textIcon + : ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(50)), + child: CachedNetworkImage( + fit: BoxFit.cover, + cacheKey: user.profileImagePath, + width: size, + height: size, + placeholder: (_, __) => Image.memory(kTransparentImage), + imageUrl: profileImageUrl, + httpHeaders: ApiService.getRequestHeaders(), + fadeInDuration: const Duration(milliseconds: 300), + errorWidget: (context, error, stackTrace) => textIcon, + ), + ), + ), + ), ); } } diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 1c7eed7e5b..d5fdf570dd 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -17,10 +17,8 @@ class ChangePasswordForm extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final passwordController = - useTextEditingController.fromValue(TextEditingValue.empty); - final confirmPasswordController = - useTextEditingController.fromValue(TextEditingValue.empty); + final passwordController = useTextEditingController.fromValue(TextEditingValue.empty); + final confirmPasswordController = useTextEditingController.fromValue(TextEditingValue.empty); final authState = ref.watch(authProvider); final formKey = GlobalKey(); @@ -72,20 +70,15 @@ class ChangePasswordForm extends HookConsumerWidget { passwordController: passwordController, onPressed: () async { if (formKey.currentState!.validate()) { - var isSuccess = await ref - .read(authProvider.notifier) - .changePassword(passwordController.value.text); + var isSuccess = + await ref.read(authProvider.notifier).changePassword(passwordController.value.text); if (isSuccess) { await ref.read(authProvider.notifier).logout(); - ref - .read(manualUploadProvider.notifier) - .cancelBackup(); + ref.read(manualUploadProvider.notifier).cancelBackup(); ref.read(backupProvider.notifier).cancelBackup(); - await ref - .read(assetProvider.notifier) - .clearAllAssets(); + await ref.read(assetProvider.notifier).clearAllAssets(); ref.read(websocketProvider.notifier).disconnect(); AutoRouter.of(context).back(); diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 5374d1ef33..7ac070b912 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; @@ -17,6 +18,7 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/migration.dart'; import 'package:immich_mobile/utils/provider_utils.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/utils/version_compatibility.dart'; @@ -41,12 +43,9 @@ class LoginForm extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final emailController = - useTextEditingController.fromValue(TextEditingValue.empty); - final passwordController = - useTextEditingController.fromValue(TextEditingValue.empty); - final serverEndpointController = - useTextEditingController.fromValue(TextEditingValue.empty); + final emailController = useTextEditingController.fromValue(TextEditingValue.empty); + final passwordController = useTextEditingController.fromValue(TextEditingValue.empty); + final serverEndpointController = useTextEditingController.fromValue(TextEditingValue.empty); final emailFocusNode = useFocusNode(); final passwordFocusNode = useFocusNode(); final serverEndpointFocusNode = useFocusNode(); @@ -100,8 +99,7 @@ class LoginForm extends HookConsumerWidget { try { isLoadingServer.value = true; - final endpoint = - await ref.read(authProvider.notifier).validateServerUrl(serverUrl); + final endpoint = await ref.read(authProvider.notifier).validateServerUrl(serverUrl); // Fetch and load server config and features await ref.read(serverInfoProvider.notifier).getServerInfo(); @@ -112,9 +110,7 @@ class LoginForm extends HookConsumerWidget { isOauthEnable.value = features.oauthEnabled; isPasswordLoginEnable.value = features.passwordLogin; - oAuthButtonLabel.value = config.oauthButtonText.isNotEmpty - ? config.oauthButtonText - : 'OAuth'; + oAuthButtonLabel.value = config.oauthButtonText.isNotEmpty ? config.oauthButtonText : 'OAuth'; serverEndpoint.value = endpoint; } on ApiException catch (e) { @@ -192,6 +188,13 @@ class LoginForm extends HookConsumerWidget { if (result.shouldChangePassword && !result.isAdmin) { context.pushRoute(const ChangePasswordRoute()); } else { + final isBeta = Store.isBetaTimelineEnabled; + if (isBeta) { + await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + await runNewSync(ref); + context.replaceRoute(const TabShellRoute()); + return; + } context.replaceRoute(const TabControllerRoute()); } } catch (error) { @@ -207,8 +210,7 @@ class LoginForm extends HookConsumerWidget { } String generateRandomString(int length) { - const chars = - 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; final random = Random.secure(); return String.fromCharCodes( Iterable.generate( @@ -292,9 +294,16 @@ class LoginForm extends HookConsumerWidget { if (isSuccess) { isLoading.value = false; final permission = ref.watch(galleryPermissionNotifier); - if (permission.isGranted || permission.isLimited) { + final isBeta = Store.isBetaTimelineEnabled; + if (!isBeta && (permission.isGranted || permission.isLimited)) { ref.watch(backupProvider.notifier).resumeBackup(); } + if (isBeta) { + await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + await runNewSync(ref); + context.replaceRoute(const TabShellRoute()); + return; + } context.replaceRoute(const TabControllerRoute()); } } catch (error, stack) { @@ -363,8 +372,7 @@ class LoginForm extends HookConsumerWidget { ), ), ), - onPressed: - isLoadingServer.value ? null : getServerAuthSettings, + onPressed: isLoadingServer.value ? null : getServerAuthSettings, icon: const Icon(Icons.arrow_forward_rounded), label: const Text( 'next', @@ -392,12 +400,12 @@ class LoginForm extends HookConsumerWidget { child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: - context.isDarkTheme ? Colors.red.shade700 : Colors.red.shade100, - borderRadius: BorderRadius.circular(8), + color: context.isDarkTheme ? Colors.red.shade700 : Colors.red.shade100, + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), border: Border.all( - color: - context.isDarkTheme ? Colors.red.shade900 : Colors.red[200]!, + color: context.isDarkTheme ? Colors.red.shade900 : Colors.red[200]!, ), ), child: Text( @@ -443,8 +451,7 @@ class LoginForm extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 18), - if (isPasswordLoginEnable.value) - LoginButton(onPressed: login), + if (isPasswordLoginEnable.value) LoginButton(onPressed: login), if (isOauthEnable.value) ...[ if (isPasswordLoginEnable.value) Padding( @@ -452,9 +459,7 @@ class LoginForm extends HookConsumerWidget { horizontal: 16.0, ), child: Divider( - color: context.isDarkTheme - ? Colors.white - : Colors.black, + color: context.isDarkTheme ? Colors.white : Colors.black, ), ), OAuthLoginButton( @@ -481,8 +486,7 @@ class LoginForm extends HookConsumerWidget { ); } - final serverSelectionOrLogin = - serverEndpoint.value == null ? buildSelectServer() : buildLogin(); + final serverSelectionOrLogin = serverEndpoint.value == null ? buildSelectServer() : buildLogin(); return LayoutBuilder( builder: (context, constraints) { diff --git a/mobile/lib/widgets/forms/login/password_input.dart b/mobile/lib/widgets/forms/login/password_input.dart index 2d2c1923f7..074899bd57 100644 --- a/mobile/lib/widgets/forms/login/password_input.dart +++ b/mobile/lib/widgets/forms/login/password_input.dart @@ -33,9 +33,7 @@ class PasswordInput extends HookConsumerWidget { suffixIcon: IconButton( onPressed: () => isPasswordVisible.value = !isPasswordVisible.value, icon: Icon( - isPasswordVisible.value - ? Icons.visibility_off_sharp - : Icons.visibility_sharp, + isPasswordVisible.value ? Icons.visibility_off_sharp : Icons.visibility_sharp, ), ), ), diff --git a/mobile/lib/widgets/forms/login/server_endpoint_input.dart b/mobile/lib/widgets/forms/login/server_endpoint_input.dart index 37bcad9d82..cddf9e9985 100644 --- a/mobile/lib/widgets/forms/login/server_endpoint_input.dart +++ b/mobile/lib/widgets/forms/login/server_endpoint_input.dart @@ -18,10 +18,7 @@ class ServerEndpointInput extends StatelessWidget { if (url == null || url.isEmpty) return null; final parsedUrl = Uri.tryParse(sanitizeUrl(url)); - if (parsedUrl == null || - !parsedUrl.isAbsolute || - !parsedUrl.scheme.startsWith("http") || - parsedUrl.host.isEmpty) { + if (parsedUrl == null || !parsedUrl.isAbsolute || !parsedUrl.scheme.startsWith("http") || parsedUrl.host.isEmpty) { return 'login_form_err_invalid_url'.tr(); } diff --git a/mobile/lib/widgets/forms/pin_input.dart b/mobile/lib/widgets/forms/pin_input.dart index 1588a65c60..6602946d7d 100644 --- a/mobile/lib/widgets/forms/pin_input.dart +++ b/mobile/lib/widgets/forms/pin_input.dart @@ -30,8 +30,7 @@ class PinInput extends StatelessWidget { final minimumPadding = 18.0; final gapWidth = 3.0; final screenWidth = context.width; - final pinWidth = - (screenWidth - (minimumPadding * 2) - (gapWidth * 5)) / (length ?? 6); + final pinWidth = (screenWidth - (minimumPadding * 2) - (gapWidth * 5)) / (length ?? 6); if (pinWidth > 60) { return const Size(60, 64); @@ -62,8 +61,7 @@ class PinInput extends StatelessWidget { if (label != null) ...[ Text( label!, - style: context.textTheme.displayLarge - ?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), + style: context.textTheme.displayLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), ), const SizedBox(height: 4), ], diff --git a/mobile/lib/widgets/forms/pin_verification_form.dart b/mobile/lib/widgets/forms/pin_verification_form.dart index f4ebf4272f..8a3f0b55df 100644 --- a/mobile/lib/widgets/forms/pin_verification_form.dart +++ b/mobile/lib/widgets/forms/pin_verification_form.dart @@ -30,8 +30,7 @@ class PinVerificationForm extends HookConsumerWidget { final isVerified = useState(false); verifyPin(String pinCode) async { - final isUnlocked = - await ref.read(authProvider.notifier).unlockPinCode(pinCode); + final isUnlocked = await ref.read(authProvider.notifier).unlockPinCode(pinCode); if (isUnlocked) { isVerified.value = true; @@ -58,9 +57,7 @@ class PinVerificationForm extends HookConsumerWidget { : Icon( icon ?? Icons.lock_outline_rounded, size: 64, - color: hasError.value - ? context.colorScheme.error - : context.primaryColor, + color: hasError.value ? context.colorScheme.error : context.primaryColor, ), ), const SizedBox(height: 36), diff --git a/mobile/lib/widgets/map/map_app_bar.dart b/mobile/lib/widgets/map/map_app_bar.dart index 4de5721486..2715386737 100644 --- a/mobile/lib/widgets/map/map_app_bar.dart +++ b/mobile/lib/widgets/map/map_app_bar.dart @@ -4,12 +4,12 @@ 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/widgets/map/map_settings_sheet.dart'; -import 'package:immich_mobile/entities/asset.entity.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; @@ -22,9 +22,8 @@ class MapAppBar extends HookWidget implements PreferredSizeWidget { padding: EdgeInsets.only(top: context.padding.top + 25), child: ValueListenableBuilder( valueListenable: selectedAssets, - builder: (ctx, value, child) => value.isNotEmpty - ? _SelectionRow(selectedAssets: selectedAssets) - : _NonSelectionRow(), + builder: (ctx, value, child) => + value.isNotEmpty ? _SelectionRow(selectedAssets: selectedAssets) : const _NonSelectionRow(), ), ); } @@ -34,6 +33,8 @@ class MapAppBar extends HookWidget implements PreferredSizeWidget { } class _NonSelectionRow extends StatelessWidget { + const _NonSelectionRow(); + @override Widget build(BuildContext context) { void onSettingsPressed() { diff --git a/mobile/lib/widgets/map/map_asset_grid.dart b/mobile/lib/widgets/map/map_asset_grid.dart index 4a3bb69cc0..ce2a486fc5 100644 --- a/mobile/lib/widgets/map/map_asset_grid.dart +++ b/mobile/lib/widgets/map/map_asset_grid.dart @@ -44,8 +44,7 @@ class MapAssetGrid extends HookConsumerWidget { final cachedRenderList = useRef(null); final lastRenderElementIndex = useRef(null); final assetInSheet = useValueNotifier(null); - final gridScrollThrottler = - useThrottler(interval: const Duration(milliseconds: 300)); + final gridScrollThrottler = useThrottler(interval: const Duration(milliseconds: 300)); // Add a cache for assets we've already loaded final assetCache = useRef>({}); @@ -67,8 +66,7 @@ class MapAssetGrid extends HookConsumerWidget { // Only fetch missing assets if (missingIds.isNotEmpty) { - final newAssets = - await ref.read(dbProvider).assets.getAllByRemoteId(missingIds); + final newAssets = await ref.read(dbProvider).assets.getAllByRemoteId(missingIds); // Add new assets to cache and current list for (final asset in newAssets) { @@ -93,8 +91,7 @@ class MapAssetGrid extends HookConsumerWidget { 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); + 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) @@ -103,8 +100,7 @@ class MapAssetGrid extends HookConsumerWidget { return; } - final renderElement = - cachedRenderList.value?.elements.elementAtOrNull(item.index); + final renderElement = cachedRenderList.value?.elements.elementAtOrNull(item.index); // Guard no render list or render element if (renderElement == null) { return; @@ -128,13 +124,9 @@ class MapAssetGrid extends HookConsumerWidget { ((renderElement.totalCount / assetsPerRow) * assetsPerRow).floor(); // trailing should never be above the totalOffset - final columnOffset = - (totalOffset - math.min(item.itemTrailingEdge, totalOffset)) ~/ - edgeOffset; + final columnOffset = (totalOffset - math.min(item.itemTrailingEdge, totalOffset)) ~/ edgeOffset; final assetOffset = rowOffset + columnOffset; - final selectedAsset = cachedRenderList.value?.allAssets - ?.elementAtOrNull(assetOffset) - ?.remoteId; + final selectedAsset = cachedRenderList.value?.allAssets?.elementAtOrNull(assetOffset)?.remoteId; if (selectedAsset != null) { onGridAssetChanged?.call(selectedAsset); @@ -154,9 +146,7 @@ class MapAssetGrid extends HookConsumerWidget { // Place it just below the drag handle heightFactor: 0.87, child: assetsInBounds.value.isNotEmpty - ? ref - .watch(assetsTimelineProvider(assetsInBounds.value)) - .when( + ? ref.watch(assetsTimelineProvider(assetsInBounds.value)).when( data: (renderList) { // Cache render list here to use it back during visibleItemsListener cachedRenderList.value = renderList; @@ -170,8 +160,7 @@ class MapAssetGrid extends HookConsumerWidget { showMultiSelectIndicator: false, selectionActive: value.isNotEmpty, listener: onAssetsSelected, - visibleItemsListener: (pos) => gridScrollThrottler - .run(() => handleVisibleItems(pos)), + visibleItemsListener: (pos) => gridScrollThrottler.run(() => handleVisibleItems(pos)), ), ); }, @@ -185,7 +174,7 @@ class MapAssetGrid extends HookConsumerWidget { }, loading: () => const SizedBox.shrink(), ) - : _MapNoAssetsInSheet(), + : const _MapNoAssetsInSheet(), ), ), _MapSheetDragRegion( @@ -201,6 +190,8 @@ class MapAssetGrid extends HookConsumerWidget { } class _MapNoAssetsInSheet extends StatelessWidget { + const _MapNoAssetsInSheet(); + @override Widget build(BuildContext context) { const image = Image( @@ -253,8 +244,7 @@ class _MapSheetDragRegion extends StatelessWidget { @override Widget build(BuildContext context) { final assetsInBoundsText = assetsInBoundCount > 0 - ? "map_assets_in_bounds" - .tr(namedArgs: {'count': assetsInBoundCount.toString()}) + ? "map_assets_in_bounds".tr(namedArgs: {'count': assetsInBoundCount.toString()}) : "map_no_assets_in_bounds".tr(); return SingleChildScrollView( @@ -285,8 +275,7 @@ class _MapSheetDragRegion extends StatelessWidget { assetsInBoundsText, style: TextStyle( fontSize: 20, - color: context.textTheme.displayLarge?.color - ?.withValues(alpha: 0.75), + color: context.textTheme.displayLarge?.color?.withValues(alpha: 0.75), fontWeight: FontWeight.w500, ), ), diff --git a/mobile/lib/widgets/map/map_bottom_sheet.dart b/mobile/lib/widgets/map/map_bottom_sheet.dart index 0249ca70dc..d8c1cc638e 100644 --- a/mobile/lib/widgets/map/map_bottom_sheet.dart +++ b/mobile/lib/widgets/map/map_bottom_sheet.dart @@ -45,8 +45,7 @@ class MapBottomSheet extends HookConsumerWidget { useOnStreamChange(mapEventStream, onData: handleMapEvents); bool onScrollNotification(DraggableScrollableNotification notification) { - isBottomSheetOpened.value = - notification.extent > (notification.maxExtent * 0.9); + isBottomSheetOpened.value = notification.extent > (notification.maxExtent * 0.9); bottomSheetOffset.value = notification.extent; // do not bubble return true; @@ -70,9 +69,7 @@ class MapBottomSheet extends HookConsumerWidget { 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, + onGridAssetChanged: (assetId) => isBottomSheetOpened.value ? onGridAssetChanged?.call(assetId) : null, onZoomToAsset: onZoomToAsset, ), ), diff --git a/mobile/lib/widgets/map/map_settings/map_settings_list_tile.dart b/mobile/lib/widgets/map/map_settings/map_settings_list_tile.dart index 1abe64ce31..5c755d80be 100644 --- a/mobile/lib/widgets/map/map_settings/map_settings_list_tile.dart +++ b/mobile/lib/widgets/map/map_settings/map_settings_list_tile.dart @@ -21,8 +21,7 @@ class MapSettingsListTile extends StatelessWidget { activeColor: context.primaryColor, title: Text( title, - style: - context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold), + style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold), ).tr(), value: selected, onChanged: onChanged, diff --git a/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart b/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart index e23716af95..a627ff8f29 100644 --- a/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart +++ b/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart @@ -81,8 +81,7 @@ class MapTimeDropDown extends StatelessWidget { ), ) .inDays, - label: "map_settings_date_range_option_years" - .tr(namedArgs: {'years': "3"}), + label: "map_settings_date_range_option_years".tr(namedArgs: {'years': "3"}), ), ], ), diff --git a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart index 19298df076..747ae06a54 100644 --- a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart +++ b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart @@ -23,8 +23,7 @@ class MapThemePicker extends StatelessWidget { child: Center( child: Text( "map_settings_theme_settings", - style: context.textTheme.bodyMedium - ?.copyWith(fontWeight: FontWeight.bold), + style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), ).tr(), ), ), @@ -79,9 +78,7 @@ class _BorderedMapThumbnail extends StatelessWidget { border: Border.fromBorderSide( BorderSide( width: 4, - color: shouldHighlight - ? context.colorScheme.onSurface - : Colors.transparent, + color: shouldHighlight ? context.colorScheme.onSurface : Colors.transparent, ), ), borderRadius: const BorderRadius.all(Radius.circular(20)), diff --git a/mobile/lib/widgets/map/map_settings_sheet.dart b/mobile/lib/widgets/map/map_settings_sheet.dart index 78d8aec75f..644056d153 100644 --- a/mobile/lib/widgets/map/map_settings_sheet.dart +++ b/mobile/lib/widgets/map/map_settings_sheet.dart @@ -26,37 +26,30 @@ class MapSettingsSheet extends HookConsumerWidget { children: [ MapThemePicker( themeMode: mapState.themeMode, - onThemeChange: (mode) => ref - .read(mapStateNotifierProvider.notifier) - .switchTheme(mode), + 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), + 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), + 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), + onChanged: (withPartners) => + ref.read(mapStateNotifierProvider.notifier).switchWithPartners(withPartners), ), MapTimeDropDown( relativeTime: mapState.relativeTime, - onTimeChange: (time) => ref - .read(mapStateNotifierProvider.notifier) - .setRelativeTime(time), + onTimeChange: (time) => ref.read(mapStateNotifierProvider.notifier).setRelativeTime(time), ), const SizedBox(height: 20), ], diff --git a/mobile/lib/widgets/map/map_theme_override.dart b/mobile/lib/widgets/map/map_theme_override.dart index 65425f9e78..3f9ae0f43f 100644 --- a/mobile/lib/widgets/map/map_theme_override.dart +++ b/mobile/lib/widgets/map/map_theme_override.dart @@ -18,25 +18,20 @@ class MapThemeOverride extends StatefulHookConsumerWidget { ConsumerState createState() => _MapThemeOverrideState(); } -class _MapThemeOverrideState extends ConsumerState - with WidgetsBindingObserver { +class _MapThemeOverrideState extends ConsumerState with WidgetsBindingObserver { late ThemeMode _theme; bool _isDarkTheme = false; - bool get _isSystemDark => - WidgetsBinding.instance.platformDispatcher.platformBrightness == - Brightness.dark; + bool get _isSystemDark => WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark; bool checkDarkTheme() { - return _theme == ThemeMode.dark || - _theme == ThemeMode.system && _isSystemDark; + return _theme == ThemeMode.dark || _theme == ThemeMode.system && _isSystemDark; } @override void initState() { super.initState(); - _theme = widget.themeMode ?? - ref.read(mapStateNotifierProvider.select((v) => v.themeMode)); + _theme = widget.themeMode ?? ref.read(mapStateNotifierProvider.select((v) => v.themeMode)); setState(() { _isDarkTheme = checkDarkTheme(); }); @@ -70,8 +65,7 @@ class _MapThemeOverrideState extends ConsumerState @override Widget build(BuildContext context) { - _theme = widget.themeMode ?? - ref.watch(mapStateNotifierProvider.select((v) => v.themeMode)); + _theme = widget.themeMode ?? ref.watch(mapStateNotifierProvider.select((v) => v.themeMode)); var appTheme = ref.watch(immichThemeProvider); final locale = ref.watch(localeProvider); diff --git a/mobile/lib/widgets/map/map_thumbnail.dart b/mobile/lib/widgets/map/map_thumbnail.dart index b225a2edcb..0dc1ad3a4f 100644 --- a/mobile/lib/widgets/map/map_thumbnail.dart +++ b/mobile/lib/widgets/map/map_thumbnail.dart @@ -4,6 +4,7 @@ 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/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart'; @@ -24,6 +25,7 @@ class MapThumbnail extends HookConsumerWidget { final double width; final ThemeMode? themeMode; final bool showAttribution; + final MapCreatedCallback? onCreated; const MapThumbnail({ super.key, @@ -36,35 +38,51 @@ class MapThumbnail extends HookConsumerWidget { this.showMarkerPin = false, this.themeMode, this.showAttribution = true, + this.onCreated, }); @override Widget build(BuildContext context, WidgetRef ref) { final offsettedCentre = LatLng(centre.latitude + 0.002, centre.longitude); final controller = useRef(null); + final styleLoaded = useState(false); final position = useValueNotifier?>(null); Future onMapCreated(MapLibreMapController mapController) async { controller.value = mapController; + styleLoaded.value = false; if (assetMarkerRemoteId != null) { // The iOS impl returns wrong toScreenLocation without the delay Future.delayed( const Duration(milliseconds: 100), - () async => - position.value = await mapController.toScreenLocation(centre), + () async => position.value = await mapController.toScreenLocation(centre), ); } + onCreated?.call(mapController); } Future onStyleLoaded() async { - if (showMarkerPin && controller.value != null) { - await controller.value?.addMarkerAtLatLng(centre); + try { + if (showMarkerPin && controller.value != null) { + await controller.value?.addMarkerAtLatLng(centre); + } + } finally { + // Calling methods on the controller after it is disposed will throw an error + // We do not have a way to check if the controller is disposed for now + // https://github.com/maplibre/flutter-maplibre-gl/issues/192 } + styleLoaded.value = true; } return MapThemeOverride( themeMode: themeMode, - mapBuilder: (style) => SizedBox( + mapBuilder: (style) => AnimatedContainer( + duration: Durations.medium2, + curve: Curves.easeOut, + foregroundDecoration: BoxDecoration( + color: context.colorScheme.inverseSurface.withAlpha(styleLoaded.value ? 0 : 200), + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), height: height, width: width, child: ClipRRect( @@ -74,8 +92,7 @@ class MapThumbnail extends HookConsumerWidget { children: [ style.widgetWhen( onData: (style) => MapLibreMap( - initialCameraPosition: - CameraPosition(target: offsettedCentre, zoom: zoom), + initialCameraPosition: CameraPosition(target: offsettedCentre, zoom: zoom), styleString: style, onMapCreated: onMapCreated, onStyleLoadedCallback: onStyleLoaded, @@ -87,13 +104,12 @@ class MapThumbnail extends HookConsumerWidget { scrollGesturesEnabled: false, rotateGesturesEnabled: false, myLocationEnabled: false, - attributionButtonMargins: - showAttribution == false ? const Point(-100, 0) : null, + attributionButtonMargins: showAttribution == false ? const Point(-100, 0) : null, ), ), ValueListenableBuilder( valueListenable: position, - builder: (_, value, __) => value != null + builder: (_, value, __) => value != null && assetMarkerRemoteId != null ? PositionedAssetMarkerIcon( size: height / 2, point: value, diff --git a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart index 2cf82517ae..6207a6ab56 100644 --- a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart +++ b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart @@ -89,8 +89,7 @@ class _AssetMarkerIcon extends StatelessWidget { imageUrl, cacheKey: cacheKey, headers: ApiService.getRequestHeaders(), - errorListener: (_) => - const Icon(Icons.image_not_supported_outlined), + errorListener: (_) => const Icon(Icons.image_not_supported_outlined), ), ), ), @@ -108,7 +107,7 @@ class _PinPainter extends CustomPainter { final double primaryRadius; final double secondaryRadius; - _PinPainter({ + const _PinPainter({ required this.primaryColor, required this.secondaryColor, required this.primaryRadius, @@ -175,7 +174,6 @@ class _PinPainter extends CustomPainter { @override bool shouldRepaint(_PinPainter old) { - return old.primaryColor != primaryColor || - old.secondaryColor != secondaryColor; + return old.primaryColor != primaryColor || old.secondaryColor != secondaryColor; } } diff --git a/mobile/lib/widgets/memories/memory_bottom_info.dart b/mobile/lib/widgets/memories/memory_bottom_info.dart index 6adf1d46b0..1797b5c1c3 100644 --- a/mobile/lib/widgets/memories/memory_bottom_info.dart +++ b/mobile/lib/widgets/memories/memory_bottom_info.dart @@ -44,8 +44,7 @@ class MemoryBottomInfo extends StatelessWidget { minWidth: 0, onPressed: () { context.maybePop(); - scrollToDateNotifierProvider - .scrollToDate(memory.assets[0].fileCreatedAt); + scrollToDateNotifierProvider.scrollToDate(memory.assets[0].fileCreatedAt); }, shape: const CircleBorder(), color: Colors.white.withValues(alpha: 0.2), diff --git a/mobile/lib/widgets/memories/memory_card.dart b/mobile/lib/widgets/memories/memory_card.dart index abe3586194..1faa114936 100644 --- a/mobile/lib/widgets/memories/memory_card.dart +++ b/mobile/lib/widgets/memories/memory_card.dart @@ -26,9 +26,9 @@ class MemoryCard extends StatelessWidget { Widget build(BuildContext context) { return Card( color: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(25.0), - side: const BorderSide( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(25.0)), + side: BorderSide( color: Colors.black, width: 1.0, ), @@ -45,11 +45,9 @@ class MemoryCard extends StatelessWidget { BoxFit fit = BoxFit.contain; if (asset.width != null && asset.height != null) { final aspectRatio = asset.width! / asset.height!; - final phoneAspectRatio = - constraints.maxWidth / constraints.maxHeight; + final phoneAspectRatio = constraints.maxWidth / constraints.maxHeight; // Look for a 25% difference in either direction - if (phoneAspectRatio * .75 < aspectRatio && - phoneAspectRatio * 1.25 > aspectRatio) { + if (phoneAspectRatio * .75 < aspectRatio && phoneAspectRatio * 1.25 > aspectRatio) { // Cover to look nice if we have nearly the same aspect ratio fit = BoxFit.cover; } diff --git a/mobile/lib/widgets/memories/memory_epilogue.dart b/mobile/lib/widgets/memories/memory_epilogue.dart index 9796bee6b1..10349aa431 100644 --- a/mobile/lib/widgets/memories/memory_epilogue.dart +++ b/mobile/lib/widgets/memories/memory_epilogue.dart @@ -11,8 +11,7 @@ class MemoryEpilogue extends StatefulWidget { State createState() => _MemoryEpilogueState(); } -class _MemoryEpilogueState extends State - with TickerProviderStateMixin { +class _MemoryEpilogueState extends State with TickerProviderStateMixin { late final _animationController = AnimationController( vsync: this, duration: const Duration( @@ -50,9 +49,7 @@ class _MemoryEpilogueState extends State children: [ Icon( Icons.check_circle_outline_sharp, - color: context.isDarkTheme - ? context.colorScheme.primary - : context.colorScheme.inversePrimary, + color: context.isDarkTheme ? context.colorScheme.primary : context.colorScheme.inversePrimary, size: 64.0, ), const SizedBox(height: 16.0), @@ -75,9 +72,7 @@ class _MemoryEpilogueState extends State child: Text( "memories_start_over", style: context.textTheme.displayMedium?.copyWith( - color: context.isDarkTheme - ? context.colorScheme.primary - : context.colorScheme.inversePrimary, + color: context.isDarkTheme ? context.colorScheme.primary : context.colorScheme.inversePrimary, ), ).tr(), ), diff --git a/mobile/lib/widgets/memories/memory_progress_indicator.dart b/mobile/lib/widgets/memories/memory_progress_indicator.dart index 438816d99c..646846cd11 100644 --- a/mobile/lib/widgets/memories/memory_progress_indicator.dart +++ b/mobile/lib/widgets/memories/memory_progress_indicator.dart @@ -27,9 +27,7 @@ class MemoryProgressIndicator extends StatelessWidget { value: value, borderRadius: const BorderRadius.all(Radius.circular(10.0)), backgroundColor: Colors.grey[800], - color: context.isDarkTheme - ? context.colorScheme.primary - : context.colorScheme.inversePrimary, + color: context.isDarkTheme ? context.colorScheme.primary : context.colorScheme.inversePrimary, ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/mobile/lib/widgets/photo_view/photo_view.dart b/mobile/lib/widgets/photo_view/photo_view.dart index f72d1e298f..0b769d559b 100644 --- a/mobile/lib/widgets/photo_view/photo_view.dart +++ b/mobile/lib/widgets/photo_view/photo_view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart'; import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_scalestate_controller.dart'; import 'package:immich_mobile/widgets/photo_view/src/core/photo_view_core.dart'; @@ -10,12 +9,16 @@ import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attri export 'src/controller/photo_view_controller.dart'; export 'src/controller/photo_view_scalestate_controller.dart'; -export 'src/core/photo_view_gesture_detector.dart' - show PhotoViewGestureDetectorScope, PhotoViewPageViewScrollPhysics; +export 'src/core/photo_view_gesture_detector.dart' show PhotoViewGestureDetectorScope, PhotoViewPageViewScrollPhysics; export 'src/photo_view_computed_scale.dart'; export 'src/photo_view_scale_state.dart'; export 'src/utils/photo_view_hero_attributes.dart'; +typedef PhotoViewControllerCallback = PhotoViewControllerBase Function(); +typedef PhotoViewControllerCallbackBuilder = void Function( + PhotoViewControllerCallback photoViewMethod, +); + /// A [StatefulWidget] that contains all the photo view rendering elements. /// /// Sample code to use within an image: @@ -239,8 +242,11 @@ class PhotoView extends StatefulWidget { this.wantKeepAlive = false, this.gaplessPlayback = false, this.heroAttributes, + this.onPageBuild, + this.controllerCallbackBuilder, this.scaleStateChangedCallback, this.enableRotation = false, + this.semanticLabel, this.controller, this.scaleStateController, this.maxScale, @@ -260,6 +266,7 @@ class PhotoView extends StatefulWidget { this.tightMode, this.filterQuality, this.disableGestures, + this.disableScaleGestures, this.errorBuilder, this.enablePanAlways, }) : child = null, @@ -278,6 +285,8 @@ class PhotoView extends StatefulWidget { this.backgroundDecoration, this.wantKeepAlive = false, this.heroAttributes, + this.onPageBuild, + this.controllerCallbackBuilder, this.scaleStateChangedCallback, this.enableRotation = false, this.controller, @@ -298,9 +307,11 @@ class PhotoView extends StatefulWidget { this.gestureDetectorBehavior, this.tightMode, this.filterQuality, + this.disableScaleGestures, this.disableGestures, this.enablePanAlways, - }) : errorBuilder = null, + }) : semanticLabel = null, + errorBuilder = null, imageProvider = null, gaplessPlayback = false, loadingBuilder = null, @@ -325,6 +336,11 @@ class PhotoView extends StatefulWidget { /// `true` -> keeps the state final bool wantKeepAlive; + /// A Semantic description of the image. + /// + /// Used to provide a description of the image to TalkBack on Android, and VoiceOver on iOS. + final String? semanticLabel; + /// This is used to continue showing the old image (`true`), or briefly show /// nothing (`false`), when the `imageProvider` changes. By default it's set /// to `false`. @@ -338,6 +354,12 @@ class PhotoView extends StatefulWidget { /// by default it is `MediaQuery.of(context).size`. final Size? customSize; + // Called when a new PhotoView widget is built + final ValueChanged? onPageBuild; + + // Called from the parent during page change to get the new controller + final PhotoViewControllerCallbackBuilder? controllerCallbackBuilder; + /// A [Function] to be called whenever the scaleState changes, this happens when the user double taps the content ou start to pinch-in. final ValueChanged? scaleStateChangedCallback; @@ -419,6 +441,9 @@ class PhotoView extends StatefulWidget { // Useful when custom gesture detector is used in child widget. final bool? disableGestures; + /// Mirror to [PhotoView.disableGestures] + final bool? disableScaleGestures; + /// Enable pan the widget even if it's smaller than the hole parent widget. /// Useful when you want to drag a widget without restrictions. final bool? enablePanAlways; @@ -435,8 +460,7 @@ class PhotoView extends StatefulWidget { } } -class _PhotoViewState extends State - with AutomaticKeepAliveClientMixin { +class _PhotoViewState extends State with AutomaticKeepAliveClientMixin { // image retrieval // controller @@ -452,6 +476,7 @@ class _PhotoViewState extends State if (widget.controller == null) { _controlledController = true; _controller = PhotoViewController(); + widget.onPageBuild?.call(_controller); } else { _controlledController = false; _controller = widget.controller!; @@ -466,6 +491,8 @@ class _PhotoViewState extends State } _scaleStateController.outputScaleStateStream.listen(scaleStateListener); + // Pass a ref to the method back to the gallery so it can fetch the controller on page changes + widget.controllerCallbackBuilder?.call(_controllerGetter); } @override @@ -474,6 +501,7 @@ class _PhotoViewState extends State if (!_controlledController) { _controlledController = true; _controller = PhotoViewController(); + widget.onPageBuild?.call(_controller); } } else { _controlledController = false; @@ -509,6 +537,8 @@ class _PhotoViewState extends State } } + PhotoViewControllerBase _controllerGetter() => _controller; + @override Widget build(BuildContext context) { super.build(context); @@ -518,8 +548,7 @@ class _PhotoViewState extends State BoxConstraints constraints, ) { final computedOuterSize = widget.customSize ?? constraints.biggest; - final backgroundDecoration = widget.backgroundDecoration ?? - const BoxDecoration(color: Colors.black); + final backgroundDecoration = widget.backgroundDecoration ?? const BoxDecoration(color: Colors.black); return widget._isCustomChild ? CustomChildWrapper( @@ -547,6 +576,7 @@ class _PhotoViewState extends State tightMode: widget.tightMode, filterQuality: widget.filterQuality, disableGestures: widget.disableGestures, + disableScaleGestures: widget.disableScaleGestures, enablePanAlways: widget.enablePanAlways, child: widget.child, ) @@ -554,6 +584,7 @@ class _PhotoViewState extends State imageProvider: widget.imageProvider!, loadingBuilder: widget.loadingBuilder, backgroundDecoration: backgroundDecoration, + semanticLabel: widget.semanticLabel, gaplessPlayback: widget.gaplessPlayback, heroAttributes: widget.heroAttributes, scaleStateChangedCallback: widget.scaleStateChangedCallback, @@ -577,6 +608,7 @@ class _PhotoViewState extends State tightMode: widget.tightMode, filterQuality: widget.filterQuality, disableGestures: widget.disableGestures, + disableScaleGestures: widget.disableScaleGestures, errorBuilder: widget.errorBuilder, enablePanAlways: widget.enablePanAlways, index: widget.index, @@ -590,14 +622,11 @@ class _PhotoViewState extends State } /// The default [ScaleStateCycle] -PhotoViewScaleState defaultScaleStateCycle(PhotoViewScaleState actual) => - switch (actual) { +PhotoViewScaleState defaultScaleStateCycle(PhotoViewScaleState actual) => switch (actual) { PhotoViewScaleState.initial => PhotoViewScaleState.covering, PhotoViewScaleState.covering => PhotoViewScaleState.originalSize, PhotoViewScaleState.originalSize => PhotoViewScaleState.initial, - PhotoViewScaleState.zoomedIn || - PhotoViewScaleState.zoomedOut => - PhotoViewScaleState.initial, + PhotoViewScaleState.zoomedIn || PhotoViewScaleState.zoomedOut => PhotoViewScaleState.initial, }; /// A type definition for a [Function] that receives the actual [PhotoViewScaleState] and returns the next one @@ -625,7 +654,8 @@ typedef PhotoViewImageTapDownCallback = Function( typedef PhotoViewImageDragStartCallback = Function( BuildContext context, DragStartDetails details, - PhotoViewControllerValue controllerValue, + PhotoViewControllerBase controllerValue, + PhotoViewScaleStateController scaleStateController, ); /// A type definition for a callback when the user drags diff --git a/mobile/lib/widgets/photo_view/photo_view_gallery.dart b/mobile/lib/widgets/photo_view/photo_view_gallery.dart index b8918309bc..3f8188f7e2 100644 --- a/mobile/lib/widgets/photo_view/photo_view_gallery.dart +++ b/mobile/lib/widgets/photo_view/photo_view_gallery.dart @@ -4,15 +4,15 @@ import 'package:immich_mobile/widgets/photo_view/photo_view.dart' show LoadingBuilder, PhotoView, + PhotoViewControllerCallback, + PhotoViewImageDragEndCallback, + PhotoViewImageDragStartCallback, + PhotoViewImageDragUpdateCallback, + PhotoViewImageLongPressStartCallback, + PhotoViewImageScaleEndCallback, PhotoViewImageTapDownCallback, PhotoViewImageTapUpCallback, - PhotoViewImageDragStartCallback, - PhotoViewImageDragEndCallback, - PhotoViewImageDragUpdateCallback, - PhotoViewImageScaleEndCallback, - PhotoViewImageLongPressStartCallback, ScaleStateCycle; - import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart'; import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_scalestate_controller.dart'; import 'package:immich_mobile/widgets/photo_view/src/core/photo_view_gesture_detector.dart'; @@ -20,7 +20,10 @@ 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'; /// A type definition for a [Function] that receives a index after a page change in [PhotoViewGallery] -typedef PhotoViewGalleryPageChangedCallback = void Function(int index); +typedef PhotoViewGalleryPageChangedCallback = void Function( + int index, + PhotoViewControllerBase? controller, +); /// A type definition for a [Function] that defines a page in [PhotoViewGallery.build] typedef PhotoViewGalleryBuilder = PhotoViewGalleryPageOptions Function( @@ -115,12 +118,14 @@ class PhotoViewGallery extends StatefulWidget { this.reverse = false, this.pageController, this.onPageChanged, + this.onPageBuild, this.scaleStateChangedCallback, this.enableRotation = false, this.scrollPhysics, this.scrollDirection = Axis.horizontal, this.customSize, this.allowImplicitScrolling = false, + this.enablePanAlways = false, }) : itemCount = null, builder = null; @@ -138,12 +143,14 @@ class PhotoViewGallery extends StatefulWidget { this.reverse = false, this.pageController, this.onPageChanged, + this.onPageBuild, this.scaleStateChangedCallback, this.enableRotation = false, this.scrollPhysics, this.scrollDirection = Axis.horizontal, this.customSize, this.allowImplicitScrolling = false, + this.enablePanAlways = false, }) : pageOptions = null, assert(itemCount != null), assert(builder != null); @@ -169,6 +176,9 @@ class PhotoViewGallery extends StatefulWidget { /// Mirror to [PhotoView.wantKeepAlive] final bool wantKeepAlive; + /// Mirror to [PhotoView.enablePanAlways] + final bool enablePanAlways; + /// Mirror to [PhotoView.gaplessPlayback] final bool gaplessPlayback; @@ -181,6 +191,9 @@ class PhotoViewGallery extends StatefulWidget { /// An callback to be called on a page change final PhotoViewGalleryPageChangedCallback? onPageChanged; + /// Mirror to [PhotoView.onPageBuild] + final ValueChanged? onPageBuild; + /// Mirror to [PhotoView.scaleStateChangedCallback] final ValueChanged? scaleStateChangedCallback; @@ -205,8 +218,8 @@ class PhotoViewGallery extends StatefulWidget { } class _PhotoViewGalleryState extends State { - late final PageController _controller = - widget.pageController ?? PageController(); + late final PageController _controller = widget.pageController ?? PageController(); + PhotoViewControllerCallback? _getController; void scaleStateChangedCallback(PhotoViewScaleState scaleState) { if (widget.scaleStateChangedCallback != null) { @@ -225,6 +238,14 @@ class _PhotoViewGalleryState extends State { return widget.pageOptions!.length; } + void _getControllerCallbackBuilder(PhotoViewControllerCallback method) { + _getController = method; + } + + void _onPageChange(int page) { + widget.onPageChanged?.call(page, _getController?.call()); + } + @override Widget build(BuildContext context) { // Enable corner hit test @@ -233,7 +254,7 @@ class _PhotoViewGalleryState extends State { child: PageView.builder( reverse: widget.reverse, controller: _controller, - onPageChanged: widget.onPageChanged, + onPageChanged: _onPageChange, itemCount: itemCount, itemBuilder: _buildItem, scrollDirection: widget.scrollDirection, @@ -249,13 +270,15 @@ class _PhotoViewGalleryState extends State { final PhotoView photoView = isCustomChild ? PhotoView.customChild( - key: ObjectKey(index), + key: pageOption.key ?? ObjectKey(index), childSize: pageOption.childSize, backgroundDecoration: widget.backgroundDecoration, wantKeepAlive: widget.wantKeepAlive, controller: pageOption.controller, scaleStateController: pageOption.scaleStateController, customSize: widget.customSize, + onPageBuild: widget.onPageBuild, + controllerCallbackBuilder: _getControllerCallbackBuilder, scaleStateChangedCallback: scaleStateChangedCallback, enableRotation: widget.enableRotation, initialScale: pageOption.initialScale, @@ -274,17 +297,22 @@ class _PhotoViewGalleryState extends State { filterQuality: pageOption.filterQuality, basePosition: pageOption.basePosition, disableGestures: pageOption.disableGestures, + disableScaleGestures: pageOption.disableScaleGestures, heroAttributes: pageOption.heroAttributes, + enablePanAlways: widget.enablePanAlways, child: pageOption.child, ) : PhotoView( - key: ObjectKey(index), + key: pageOption.key ?? ObjectKey(index), index: index, imageProvider: pageOption.imageProvider, loadingBuilder: widget.loadingBuilder, backgroundDecoration: widget.backgroundDecoration, + semanticLabel: pageOption.semanticLabel, wantKeepAlive: widget.wantKeepAlive, controller: pageOption.controller, + onPageBuild: widget.onPageBuild, + controllerCallbackBuilder: _getControllerCallbackBuilder, scaleStateController: pageOption.scaleStateController, customSize: widget.customSize, gaplessPlayback: widget.gaplessPlayback, @@ -306,6 +334,8 @@ class _PhotoViewGalleryState extends State { filterQuality: pageOption.filterQuality, basePosition: pageOption.basePosition, disableGestures: pageOption.disableGestures, + disableScaleGestures: pageOption.disableScaleGestures, + enablePanAlways: widget.enablePanAlways, errorBuilder: pageOption.errorBuilder, heroAttributes: pageOption.heroAttributes, ); @@ -332,9 +362,10 @@ class _PhotoViewGalleryState extends State { /// class PhotoViewGalleryPageOptions { PhotoViewGalleryPageOptions({ - Key? key, + this.key, required this.imageProvider, this.heroAttributes, + this.semanticLabel, this.minScale, this.maxScale, this.initialScale, @@ -352,15 +383,18 @@ class PhotoViewGalleryPageOptions { this.gestureDetectorBehavior, this.tightMode, this.filterQuality, + this.disableScaleGestures, this.disableGestures, this.errorBuilder, }) : child = null, childSize = null, assert(imageProvider != null); - PhotoViewGalleryPageOptions.customChild({ + const PhotoViewGalleryPageOptions.customChild({ + this.key, required this.child, this.childSize, + this.semanticLabel, this.heroAttributes, this.minScale, this.maxScale, @@ -379,16 +413,22 @@ class PhotoViewGalleryPageOptions { this.gestureDetectorBehavior, this.tightMode, this.filterQuality, + this.disableScaleGestures, this.disableGestures, }) : errorBuilder = null, imageProvider = null; + final Key? key; + /// Mirror to [PhotoView.imageProvider] final ImageProvider? imageProvider; /// Mirror to [PhotoView.heroAttributes] final PhotoViewHeroAttributes? heroAttributes; + /// Mirror to [PhotoView.semanticLabel] + final String? semanticLabel; + /// Mirror to [PhotoView.minScale] final dynamic minScale; @@ -446,6 +486,9 @@ class PhotoViewGalleryPageOptions { /// Mirror to [PhotoView.disableGestures] final bool? disableGestures; + /// Mirror to [PhotoView.disableGestures] + final bool? disableScaleGestures; + /// Quality levels for image filters. final FilterQuality? filterQuality; diff --git a/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart b/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart index e26708bb41..6a860695b2 100644 --- a/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart +++ b/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart @@ -37,6 +37,13 @@ abstract class PhotoViewControllerBase { /// Closes streams and removes eventual listeners. void dispose(); + void positionAnimationBuilder(void Function(Offset)? value); + void scaleAnimationBuilder(void Function(double)? value); + void rotationAnimationBuilder(void Function(double)? value); + + /// Animates multiple fields of the state + void animateMultiple({Offset? position, double? scale, double? rotation}); + /// Add a listener that will ignore updates made internally /// /// Since it is made for internal use, it is not performatic to use more than one @@ -99,11 +106,7 @@ class PhotoViewControllerValue { rotationFocusPoint == other.rotationFocusPoint; @override - int get hashCode => - position.hashCode ^ - scale.hashCode ^ - rotation.hashCode ^ - rotationFocusPoint.hashCode; + int get hashCode => position.hashCode ^ scale.hashCode ^ rotation.hashCode ^ rotationFocusPoint.hashCode; @override String toString() { @@ -118,8 +121,7 @@ class PhotoViewControllerValue { /// /// For details of fields and methods, check [PhotoViewControllerBase]. /// -class PhotoViewController - implements PhotoViewControllerBase { +class PhotoViewController implements PhotoViewControllerBase { PhotoViewController({ Offset initialPosition = Offset.zero, double initialRotation = 0.0, @@ -147,12 +149,31 @@ class PhotoViewController late StreamController _outputCtrl; + late void Function(Offset)? _animatePosition; + late void Function(double)? _animateScale; + late void Function(double)? _animateRotation; + @override Stream get outputStateStream => _outputCtrl.stream; @override late PhotoViewControllerValue prevValue; + @override + void positionAnimationBuilder(void Function(Offset)? value) { + _animatePosition = value; + } + + @override + void scaleAnimationBuilder(void Function(double)? value) { + _animateScale = value; + } + + @override + void rotationAnimationBuilder(void Function(double)? value) { + _animateRotation = value; + } + @override void reset() { value = initial; @@ -172,6 +193,21 @@ class PhotoViewController _valueNotifier.removeIgnorableListener(callback); } + @override + void animateMultiple({Offset? position, double? scale, double? rotation}) { + if (position != null && _animatePosition != null) { + _animatePosition!(position); + } + + if (scale != null && _animateScale != null) { + _animateScale!(scale); + } + + if (rotation != null && _animateRotation != null) { + _animateRotation!(rotation); + } + } + @override void dispose() { _outputCtrl.close(); diff --git a/mobile/lib/widgets/photo_view/src/controller/photo_view_controller_delegate.dart b/mobile/lib/widgets/photo_view/src/controller/photo_view_controller_delegate.dart index 968ac652e7..d28577ea49 100644 --- a/mobile/lib/widgets/photo_view/src/controller/photo_view_controller_delegate.dart +++ b/mobile/lib/widgets/photo_view/src/controller/photo_view_controller_delegate.dart @@ -1,10 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart' - show - PhotoViewControllerBase, - PhotoViewScaleState, - PhotoViewScaleStateController, - ScaleStateCycle; + show PhotoViewControllerBase, PhotoViewScaleState, PhotoViewScaleStateController, ScaleStateCycle; import 'package:immich_mobile/widgets/photo_view/src/core/photo_view_core.dart'; import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_utils.dart'; @@ -14,8 +10,7 @@ import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_utils.dart mixin PhotoViewControllerDelegate on State { PhotoViewControllerBase get controller => widget.controller; - PhotoViewScaleStateController get scaleStateController => - widget.scaleStateController; + PhotoViewScaleStateController get scaleStateController => widget.scaleStateController; ScaleBoundaries get scaleBoundaries => widget.scaleBoundaries; @@ -68,9 +63,7 @@ mixin PhotoViewControllerDelegate on State { return; } final PhotoViewScaleState newScaleState = - (scale > scaleBoundaries.initialScale) - ? PhotoViewScaleState.zoomedIn - : PhotoViewScaleState.zoomedOut; + (scale > scaleBoundaries.initialScale) ? PhotoViewScaleState.zoomedIn : PhotoViewScaleState.zoomedOut; scaleStateController.setInvisibly(newScaleState); } @@ -79,8 +72,7 @@ mixin PhotoViewControllerDelegate on State { double get scale { // for figuring out initial scale - final needsRecalc = markNeedsScaleRecalc && - !scaleStateController.scaleState.isScaleStateZooming; + final needsRecalc = markNeedsScaleRecalc && !scaleStateController.scaleState.isScaleStateZooming; final scaleExistsOnController = controller.scale != null; if (needsRecalc || !scaleExistsOnController) { @@ -111,20 +103,27 @@ mixin PhotoViewControllerDelegate on State { ); } + PhotoViewScaleState getScaleStateFromNewScale(double newScale) { + PhotoViewScaleState newScaleState = PhotoViewScaleState.initial; + if (scale != scaleBoundaries.initialScale) { + newScaleState = + (newScale > scaleBoundaries.initialScale) ? PhotoViewScaleState.zoomedIn : PhotoViewScaleState.zoomedOut; + } + return newScaleState; + } + void updateScaleStateFromNewScale(double newScale) { PhotoViewScaleState newScaleState = PhotoViewScaleState.initial; if (scale != scaleBoundaries.initialScale) { - newScaleState = (newScale > scaleBoundaries.initialScale) - ? PhotoViewScaleState.zoomedIn - : PhotoViewScaleState.zoomedOut; + newScaleState = + (newScale > scaleBoundaries.initialScale) ? PhotoViewScaleState.zoomedIn : PhotoViewScaleState.zoomedOut; } scaleStateController.setInvisibly(newScaleState); } void nextScaleState() { final PhotoViewScaleState scaleState = scaleStateController.scaleState; - if (scaleState == PhotoViewScaleState.zoomedIn || - scaleState == PhotoViewScaleState.zoomedOut) { + if (scaleState == PhotoViewScaleState.zoomedIn || scaleState == PhotoViewScaleState.zoomedOut) { scaleStateController.scaleState = scaleStateCycle(scaleState); return; } diff --git a/mobile/lib/widgets/photo_view/src/controller/photo_view_scalestate_controller.dart b/mobile/lib/widgets/photo_view/src/controller/photo_view_scalestate_controller.dart index 16021ceab1..e96aff7780 100644 --- a/mobile/lib/widgets/photo_view/src/controller/photo_view_scalestate_controller.dart +++ b/mobile/lib/widgets/photo_view/src/controller/photo_view_scalestate_controller.dart @@ -20,15 +20,14 @@ typedef ScaleStateListener = void Function(double prevScale, double nextScale); /// class PhotoViewScaleStateController { late final IgnorableValueNotifier _scaleStateNotifier = - IgnorableValueNotifier(PhotoViewScaleState.initial) - ..addListener(_scaleStateChangeListener); - final StreamController _outputScaleStateCtrl = - StreamController.broadcast() - ..sink.add(PhotoViewScaleState.initial); + IgnorableValueNotifier(PhotoViewScaleState.initial)..addListener(_scaleStateChangeListener); + final StreamController _outputScaleStateCtrl = StreamController.broadcast() + ..sink.add(PhotoViewScaleState.initial); + + bool _hasZoomedOutManually = false; /// The output for state/value updates - Stream get outputScaleStateStream => - _outputScaleStateCtrl.stream; + Stream get outputScaleStateStream => _outputScaleStateCtrl.stream; /// The state value before the last change or the initial state if the state has not been changed. PhotoViewScaleState prevScaleState = PhotoViewScaleState.initial; @@ -42,17 +41,25 @@ class PhotoViewScaleStateController { return; } + if (newValue == PhotoViewScaleState.zoomedOut) { + _hasZoomedOutManually = true; + } + + if (newValue == PhotoViewScaleState.initial) { + _hasZoomedOutManually = false; + } + prevScaleState = _scaleStateNotifier.value; _scaleStateNotifier.value = newValue; } + bool get hasZoomedOutManually => _hasZoomedOutManually; + /// Checks if its actual value is different than previousValue bool get hasChanged => prevScaleState != scaleState; /// Check if is `zoomedIn` & `zoomedOut` - bool get isZooming => - scaleState == PhotoViewScaleState.zoomedIn || - scaleState == PhotoViewScaleState.zoomedOut; + bool get isZooming => scaleState == PhotoViewScaleState.zoomedIn || scaleState == PhotoViewScaleState.zoomedOut; /// Resets the state to the initial value; void reset() { @@ -71,6 +78,15 @@ class PhotoViewScaleStateController { if (_scaleStateNotifier.value == newValue) { return; } + + if (newValue == PhotoViewScaleState.zoomedOut) { + _hasZoomedOutManually = true; + } + + if (newValue == PhotoViewScaleState.initial) { + _hasZoomedOutManually = false; + } + prevScaleState = _scaleStateNotifier.value; _scaleStateNotifier.updateIgnoring(newValue); } diff --git a/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart b/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart index bb892737f6..e1ec36862a 100644 --- a/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart +++ b/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart @@ -29,6 +29,7 @@ class PhotoViewCore extends StatefulWidget { super.key, required this.imageProvider, required this.backgroundDecoration, + required this.semanticLabel, required this.gaplessPlayback, required this.heroAttributes, required this.enableRotation, @@ -48,6 +49,7 @@ class PhotoViewCore extends StatefulWidget { required this.tightMode, required this.filterQuality, required this.disableGestures, + required this.disableScaleGestures, required this.enablePanAlways, }) : customChild = null; @@ -73,12 +75,15 @@ class PhotoViewCore extends StatefulWidget { required this.tightMode, required this.filterQuality, required this.disableGestures, + required this.disableScaleGestures, required this.enablePanAlways, - }) : imageProvider = null, + }) : semanticLabel = null, + imageProvider = null, gaplessPlayback = false; final Decoration? backgroundDecoration; final ImageProvider? imageProvider; + final String? semanticLabel; final bool? gaplessPlayback; final PhotoViewHeroAttributes? heroAttributes; final bool enableRotation; @@ -103,6 +108,7 @@ class PhotoViewCore extends StatefulWidget { final HitTestBehavior? gestureDetectorBehavior; final bool tightMode; final bool disableGestures; + final bool disableScaleGestures; final bool enablePanAlways; final FilterQuality filterQuality; @@ -116,10 +122,8 @@ class PhotoViewCore extends StatefulWidget { } class PhotoViewCoreState extends State - with - TickerProviderStateMixin, - PhotoViewControllerDelegate, - HitCornersDetector { + with TickerProviderStateMixin, PhotoViewControllerDelegate, HitCornersDetector { + Offset? _normalizedPosition; double? _scaleBefore; double? _rotationBefore; @@ -129,8 +133,8 @@ class PhotoViewCoreState extends State late final AnimationController _positionAnimationController; Animation? _positionAnimation; - late final AnimationController _rotationAnimationController = - AnimationController(vsync: this)..addListener(handleRotationAnimation); + late final AnimationController _rotationAnimationController = AnimationController(vsync: this) + ..addListener(handleRotationAnimation); Animation? _rotationAnimation; PhotoViewHeroAttributes? get heroAttributes => widget.heroAttributes; @@ -152,32 +156,31 @@ class PhotoViewCoreState extends State void onScaleStart(ScaleStartDetails details) { _rotationBefore = controller.rotation; _scaleBefore = scale; + _normalizedPosition = details.focalPoint - controller.position; _scaleAnimationController.stop(); _positionAnimationController.stop(); _rotationAnimationController.stop(); } + bool _shouldAllowPanRotate() => switch (scaleStateController.scaleState) { + PhotoViewScaleState.zoomedIn => scaleStateController.hasZoomedOutManually, + _ => true, + }; + void onScaleUpdate(ScaleUpdateDetails details) { - final centeredFocalPoint = Offset( - details.focalPoint.dx - scaleBoundaries.outerSize.width / 2, - details.focalPoint.dy - scaleBoundaries.outerSize.height / 2, - ); final double newScale = _scaleBefore! * details.scale; - final double scaleDelta = newScale / scale; - final Offset newPosition = - (controller.position + details.focalPointDelta) * scaleDelta - - centeredFocalPoint * (scaleDelta - 1); + Offset delta = details.focalPoint - _normalizedPosition!; updateScaleStateFromNewScale(newScale); + final panEnabled = widget.enablePanAlways && _shouldAllowPanRotate(); + final rotationEnabled = widget.enableRotation && _shouldAllowPanRotate(); + updateMultiple( scale: newScale, - position: widget.enablePanAlways - ? newPosition - : clampPosition(position: newPosition), - rotation: - widget.enableRotation ? _rotationBefore! + details.rotation : null, - rotationFocusPoint: widget.enableRotation ? details.focalPoint : null, + position: panEnabled ? delta : clampPosition(position: delta * details.scale), + rotation: rotationEnabled ? _rotationBefore! + details.rotation : null, + rotationFocusPoint: rotationEnabled ? details.focalPoint : null, ); } @@ -189,6 +192,16 @@ class PhotoViewCoreState extends State widget.onScaleEnd?.call(context, details, controller.value); + final scaleState = getScaleStateFromNewScale(scale); + if (scaleState == PhotoViewScaleState.zoomedOut) { + scaleStateController.scaleState = PhotoViewScaleState.originalSize; + } else if (scaleState == PhotoViewScaleState.zoomedIn) { + animateRotation(controller.rotation, 0); + if (_shouldAllowPanRotate()) { + animatePosition(controller.position, Offset.zero); + } + } + //animate back to maxScale if gesture exceeded the maxScale specified if (s > maxScale) { final double scaleComebackRatio = maxScale / s; @@ -232,6 +245,9 @@ class PhotoViewCoreState extends State } void animateScale(double from, double to) { + if (!mounted) { + return; + } _scaleAnimation = Tween( begin: from, end: to, @@ -242,16 +258,20 @@ class PhotoViewCoreState extends State } void animatePosition(Offset from, Offset to) { - _positionAnimation = Tween(begin: from, end: to) - .animate(_positionAnimationController); + if (!mounted) { + return; + } + _positionAnimation = Tween(begin: from, end: to).animate(_positionAnimationController); _positionAnimationController ..value = 0.0 ..fling(velocity: 0.4); } void animateRotation(double from, double to) { - _rotationAnimation = Tween(begin: from, end: to) - .animate(_rotationAnimationController); + if (!mounted) { + return; + } + _rotationAnimation = Tween(begin: from, end: to).animate(_rotationAnimationController); _rotationAnimationController ..value = 0.0 ..fling(velocity: 0.4); @@ -265,25 +285,40 @@ class PhotoViewCoreState extends State /// Check if scale is equal to initial after scale animation update void onAnimationStatusCompleted() { - if (scaleStateController.scaleState != PhotoViewScaleState.initial && - scale == scaleBoundaries.initialScale) { + if (scaleStateController.scaleState != PhotoViewScaleState.initial && scale == scaleBoundaries.initialScale) { scaleStateController.setInvisibly(PhotoViewScaleState.initial); } } + void _animateControllerPosition(Offset position) { + animatePosition(controller.position, position); + } + + void _animateControllerScale(double scale) { + if (controller.scale != null) { + animateScale(controller.scale!, scale); + } + } + + void _animateControllerRotation(double rotation) { + animateRotation(controller.rotation, rotation); + } + @override void initState() { super.initState(); initDelegate(); addAnimateOnScaleStateUpdate(animateOnScaleStateUpdate); + controller.positionAnimationBuilder(_animateControllerPosition); + controller.scaleAnimationBuilder(_animateControllerScale); + controller.rotationAnimationBuilder(_animateControllerRotation); cachedScaleBoundaries = widget.scaleBoundaries; _scaleAnimationController = AnimationController(vsync: this) ..addListener(handleScaleAnimation) ..addStatusListener(onAnimationStatus); - _positionAnimationController = AnimationController(vsync: this) - ..addListener(handlePositionAnimate); + _positionAnimationController = AnimationController(vsync: this)..addListener(handlePositionAnimate); } void animateOnScaleStateUpdate(double prevScale, double nextScale) { @@ -341,13 +376,11 @@ class PhotoViewCoreState extends State basePosition, useImageScale, ), - child: _buildHero(), + child: _buildHero(_buildChild()), ); final child = Container( - constraints: widget.tightMode - ? BoxConstraints.tight(scaleBoundaries.childSize * scale) - : null, + constraints: widget.tightMode ? BoxConstraints.tight(scaleBoundaries.childSize * scale) : null, decoration: widget.backgroundDecoration ?? _defaultDecoration, child: Center( child: Transform( @@ -363,29 +396,34 @@ class PhotoViewCoreState extends State } return PhotoViewGestureDetector( - onDoubleTap: nextScaleState, - onScaleStart: onScaleStart, - onScaleUpdate: onScaleUpdate, - onScaleEnd: onScaleEnd, + disableScaleGestures: widget.disableScaleGestures, + onDoubleTap: widget.disableScaleGestures ? null : onDoubleTap, + onScaleStart: widget.disableScaleGestures ? null : onScaleStart, + onScaleUpdate: widget.disableScaleGestures ? null : onScaleUpdate, + onScaleEnd: widget.disableScaleGestures ? null : onScaleEnd, onDragStart: widget.onDragStart != null - ? (details) => widget.onDragStart!(context, details, value) + ? (details) => widget.onDragStart!( + context, + details, + widget.controller, + widget.scaleStateController, + ) : null, onDragEnd: widget.onDragEnd != null - ? (details) => widget.onDragEnd!(context, details, value) + ? (details) => widget.onDragEnd!(context, details, widget.controller.value) : null, onDragUpdate: widget.onDragUpdate != null - ? (details) => widget.onDragUpdate!(context, details, value) + ? (details) => widget.onDragUpdate!( + context, + details, + widget.controller.value, + ) : null, hitDetector: this, - onTapUp: widget.onTapUp != null - ? (details) => widget.onTapUp!(context, details, value) - : null, - onTapDown: widget.onTapDown != null - ? (details) => widget.onTapDown!(context, details, value) - : null, - onLongPressStart: widget.onLongPressStart != null - ? (details) => widget.onLongPressStart!(context, details, value) - : null, + onTapUp: widget.onTapUp != null ? (details) => widget.onTapUp!(context, details, value) : null, + onTapDown: widget.onTapDown != null ? (details) => widget.onTapDown!(context, details, value) : null, + onLongPressStart: + widget.onLongPressStart != null ? (details) => widget.onLongPressStart!(context, details, value) : null, child: child, ); } else { @@ -395,7 +433,7 @@ class PhotoViewCoreState extends State ); } - Widget _buildHero() { + Widget _buildHero(Widget child) { return heroAttributes != null ? Hero( tag: heroAttributes!.tag, @@ -403,16 +441,18 @@ class PhotoViewCoreState extends State flightShuttleBuilder: heroAttributes!.flightShuttleBuilder, placeholderBuilder: heroAttributes!.placeholderBuilder, transitionOnUserGestures: heroAttributes!.transitionOnUserGestures, - child: _buildChild(), + child: child, ) - : _buildChild(); + : child; } Widget _buildChild() { return widget.hasCustomChild ? widget.customChild! : Image( + key: widget.heroAttributes?.tag != null ? ObjectKey(widget.heroAttributes!.tag) : null, image: widget.imageProvider!, + semanticLabel: widget.semanticLabel, gaplessPlayback: widget.gaplessPlayback ?? false, filterQuality: widget.filterQuality, width: scaleBoundaries.childSize.width * scale, @@ -442,14 +482,13 @@ class _CenterWithOriginalSizeDelegate extends SingleChildLayoutDelegate { final double offsetX = halfWidth * (basePosition.x + 1); final double offsetY = halfHeight * (basePosition.y + 1); + return Offset(offsetX, offsetY); } @override BoxConstraints getConstraintsForChild(BoxConstraints constraints) { - return useImageScale - ? const BoxConstraints() - : BoxConstraints.tight(subjectSize); + return useImageScale ? const BoxConstraints() : BoxConstraints.tight(subjectSize); } @override @@ -467,6 +506,5 @@ class _CenterWithOriginalSizeDelegate extends SingleChildLayoutDelegate { useImageScale == other.useImageScale; @override - int get hashCode => - subjectSize.hashCode ^ basePosition.hashCode ^ useImageScale.hashCode; + int get hashCode => subjectSize.hashCode ^ basePosition.hashCode ^ useImageScale.hashCode; } diff --git a/mobile/lib/widgets/photo_view/src/core/photo_view_gesture_detector.dart b/mobile/lib/widgets/photo_view/src/core/photo_view_gesture_detector.dart index 2eef5e6742..6f456713a9 100644 --- a/mobile/lib/widgets/photo_view/src/core/photo_view_gesture_detector.dart +++ b/mobile/lib/widgets/photo_view/src/core/photo_view_gesture_detector.dart @@ -21,6 +21,7 @@ class PhotoViewGestureDetector extends StatelessWidget { this.onTapUp, this.onTapDown, this.behavior, + this.disableScaleGestures = false, }); final GestureDoubleTapCallback? onDoubleTap; @@ -43,6 +44,8 @@ class PhotoViewGestureDetector extends StatelessWidget { final HitTestBehavior? behavior; + final bool disableScaleGestures; + @override Widget build(BuildContext context) { final scope = PhotoViewGestureDetectorScope.of(context); @@ -50,12 +53,10 @@ class PhotoViewGestureDetector extends StatelessWidget { final Axis? axis = scope?.axis; final touchSlopFactor = scope?.touchSlopFactor ?? 2; - final Map gestures = - {}; + final Map gestures = {}; if (onTapDown != null || onTapUp != null) { - gestures[TapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( + gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(debugOwner: this), (TapGestureRecognizer instance) { instance @@ -66,8 +67,7 @@ class PhotoViewGestureDetector extends StatelessWidget { } if (onDragStart != null || onDragEnd != null || onDragUpdate != null) { - gestures[VerticalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( + gestures[VerticalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => VerticalDragGestureRecognizer(debugOwner: this), (VerticalDragGestureRecognizer instance) { instance @@ -78,16 +78,14 @@ class PhotoViewGestureDetector extends StatelessWidget { ); } - gestures[DoubleTapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( + gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => DoubleTapGestureRecognizer(debugOwner: this), (DoubleTapGestureRecognizer instance) { instance.onDoubleTap = onDoubleTap; }, ); - gestures[PhotoViewGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( + gestures[PhotoViewGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => PhotoViewGestureRecognizer( hitDetector: hitDetector, debugOwner: this, @@ -96,16 +94,16 @@ class PhotoViewGestureDetector extends StatelessWidget { ), (PhotoViewGestureRecognizer instance) { instance + ..dragStartBehavior = DragStartBehavior.start ..onStart = onScaleStart ..onUpdate = onScaleUpdate - ..onEnd = onScaleEnd; + ..onEnd = onScaleEnd + ..disableScaleGestures = disableScaleGestures; }, ); - gestures[LongPressGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => LongPressGestureRecognizer(debugOwner: this), - (LongPressGestureRecognizer instance) { + gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer(debugOwner: this), (LongPressGestureRecognizer instance) { instance.onLongPressStart = onLongPressStart; }); @@ -124,10 +122,12 @@ class PhotoViewGestureRecognizer extends ScaleGestureRecognizer { this.validateAxis, this.touchSlopFactor = 1, PointerDeviceKind? kind, + this.disableScaleGestures = false, }) : super(supportedDevices: null); final HitCornersDetector? hitDetector; final Axis? validateAxis; final double touchSlopFactor; + bool disableScaleGestures; Map _pointerLocations = {}; @@ -155,7 +155,7 @@ class PhotoViewGestureRecognizer extends ScaleGestureRecognizer { @override void handleEvent(PointerEvent event) { - if (validateAxis != null) { + if (validateAxis != null && !disableScaleGestures) { bool didChangeConfiguration = false; if (event is PointerMoveEvent) { if (!event.synthesized) { @@ -191,16 +191,14 @@ class PhotoViewGestureRecognizer extends ScaleGestureRecognizer { for (final int pointer in _pointerLocations.keys) { focalPoint += _pointerLocations[pointer]!; } - _currentFocalPoint = - count > 0 ? focalPoint / count.toDouble() : Offset.zero; + _currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero; // Span is the average deviation from focal point. Horizontal and vertical // spans are the average deviations from the focal point's horizontal and // vertical coordinates, respectively. double totalDeviation = 0.0; for (final int pointer in _pointerLocations.keys) { - totalDeviation += - (_currentFocalPoint! - _pointerLocations[pointer]!).distance; + totalDeviation += (_currentFocalPoint! - _pointerLocations[pointer]!).distance; } _currentSpan = count > 0 ? totalDeviation / count : 0.0; } @@ -212,15 +210,13 @@ class PhotoViewGestureRecognizer extends ScaleGestureRecognizer { : hitDetector!.shouldMove(move, Axis.horizontal); if (shouldMove || _pointerLocations.keys.length > 1) { final double spanDelta = (_currentSpan! - _initialSpan!).abs(); - final double focalPointDelta = - (_currentFocalPoint! - _initialFocalPoint!).distance; + final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint!).distance; // warning: do not compare `focalPointDelta` to `kPanSlop` // `ScaleGestureRecognizer` uses `kPanSlop`, but `HorizontalDragGestureRecognizer` uses `kTouchSlop` // and PhotoView recognizer may compete with the `HorizontalDragGestureRecognizer` from a containing `PageView` // setting `touchSlopFactor` to 2 restores default `ScaleGestureRecognizer` behaviour as `kPanSlop = kTouchSlop * 2.0` // setting `touchSlopFactor` in [0, 1] will allow this recognizer to accept the gesture before the one from `PageView` - if (spanDelta > kScaleSlop || - focalPointDelta > kTouchSlop * touchSlopFactor) { + if (spanDelta > kScaleSlop || focalPointDelta > kTouchSlop * touchSlopFactor) { acceptGesture(event.pointer); } } @@ -253,8 +249,8 @@ class PhotoViewGestureDetectorScope extends InheritedWidget { }); static PhotoViewGestureDetectorScope? of(BuildContext context) { - final PhotoViewGestureDetectorScope? scope = context - .dependOnInheritedWidgetOfExactType(); + final PhotoViewGestureDetectorScope? scope = + context.dependOnInheritedWidgetOfExactType(); return scope; } @@ -268,8 +264,7 @@ class PhotoViewGestureDetectorScope extends InheritedWidget { @override bool updateShouldNotify(PhotoViewGestureDetectorScope oldWidget) { - return axis != oldWidget.axis && - touchSlopFactor != oldWidget.touchSlopFactor; + return axis != oldWidget.axis && touchSlopFactor != oldWidget.touchSlopFactor; } } diff --git a/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart b/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart index 768e5d9cc7..b02b7feb68 100644 --- a/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart +++ b/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart @@ -35,8 +35,7 @@ mixin HitCornersDetector on PhotoViewControllerDelegate { if (!hitCorners.hasHitAny) { return true; } - final axisBlocked = hitCorners.hasHitBoth || - (hitCorners.hasHitMax ? mainAxisMove > 0 : mainAxisMove < 0); + final axisBlocked = hitCorners.hasHitBoth || (hitCorners.hasHitMax ? mainAxisMove > 0 : mainAxisMove < 0); if (axisBlocked) { return false; } diff --git a/mobile/lib/widgets/photo_view/src/photo_view_computed_scale.dart b/mobile/lib/widgets/photo_view/src/photo_view_computed_scale.dart index a01db562c7..52bb8a0a50 100644 --- a/mobile/lib/widgets/photo_view/src/photo_view_computed_scale.dart +++ b/mobile/lib/widgets/photo_view/src/photo_view_computed_scale.dart @@ -27,9 +27,7 @@ class PhotoViewComputedScale { @override bool operator ==(Object other) => identical(this, other) || - other is PhotoViewComputedScale && - runtimeType == other.runtimeType && - _value == other._value; + other is PhotoViewComputedScale && runtimeType == other.runtimeType && _value == other._value; @override int get hashCode => _value.hashCode; diff --git a/mobile/lib/widgets/photo_view/src/photo_view_default_widgets.dart b/mobile/lib/widgets/photo_view/src/photo_view_default_widgets.dart index a843087bad..912fb5e839 100644 --- a/mobile/lib/widgets/photo_view/src/photo_view_default_widgets.dart +++ b/mobile/lib/widgets/photo_view/src/photo_view_default_widgets.dart @@ -29,9 +29,7 @@ class PhotoViewDefaultLoading extends StatelessWidget { Widget build(BuildContext context) { final expectedBytes = event?.expectedTotalBytes; final loadedBytes = event?.cumulativeBytesLoaded; - final value = loadedBytes != null && expectedBytes != null - ? loadedBytes / expectedBytes - : null; + final value = loadedBytes != null && expectedBytes != null ? loadedBytes / expectedBytes : null; return Center( child: SizedBox( diff --git a/mobile/lib/widgets/photo_view/src/photo_view_scale_state.dart b/mobile/lib/widgets/photo_view/src/photo_view_scale_state.dart index fc6d4db3f9..0d1d4715e8 100644 --- a/mobile/lib/widgets/photo_view/src/photo_view_scale_state.dart +++ b/mobile/lib/widgets/photo_view/src/photo_view_scale_state.dart @@ -6,7 +6,5 @@ enum PhotoViewScaleState { zoomedIn, zoomedOut; - bool get isScaleStateZooming => - this == PhotoViewScaleState.zoomedIn || - this == PhotoViewScaleState.zoomedOut; + bool get isScaleStateZooming => this == PhotoViewScaleState.zoomedIn || this == PhotoViewScaleState.zoomedOut; } diff --git a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart index 57496f3777..d4afe85d2b 100644 --- a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart +++ b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart @@ -11,6 +11,7 @@ class ImageWrapper extends StatefulWidget { required this.imageProvider, required this.loadingBuilder, required this.backgroundDecoration, + required this.semanticLabel, required this.gaplessPlayback, required this.heroAttributes, required this.scaleStateChangedCallback, @@ -34,6 +35,7 @@ class ImageWrapper extends StatefulWidget { required this.tightMode, required this.filterQuality, required this.disableGestures, + this.disableScaleGestures, required this.errorBuilder, required this.enablePanAlways, required this.index, @@ -43,6 +45,7 @@ class ImageWrapper extends StatefulWidget { final LoadingBuilder? loadingBuilder; final ImageErrorWidgetBuilder? errorBuilder; final BoxDecoration backgroundDecoration; + final String? semanticLabel; final bool gaplessPlayback; final PhotoViewHeroAttributes? heroAttributes; final ValueChanged? scaleStateChangedCallback; @@ -66,6 +69,7 @@ class ImageWrapper extends StatefulWidget { final bool? tightMode; final FilterQuality? filterQuality; final bool? disableGestures; + final bool? disableScaleGestures; final bool? enablePanAlways; final int index; @@ -193,6 +197,7 @@ class _ImageWrapperState extends State { return PhotoViewCore( imageProvider: widget.imageProvider, backgroundDecoration: widget.backgroundDecoration, + semanticLabel: widget.semanticLabel, gaplessPlayback: widget.gaplessPlayback, enableRotation: widget.enableRotation, heroAttributes: widget.heroAttributes, @@ -212,6 +217,7 @@ class _ImageWrapperState extends State { tightMode: widget.tightMode ?? false, filterQuality: widget.filterQuality ?? FilterQuality.none, disableGestures: widget.disableGestures ?? false, + disableScaleGestures: widget.disableScaleGestures ?? false, enablePanAlways: widget.enablePanAlways ?? false, ); } @@ -266,6 +272,7 @@ class CustomChildWrapper extends StatelessWidget { required this.tightMode, required this.filterQuality, required this.disableGestures, + this.disableScaleGestures, required this.enablePanAlways, }); @@ -296,6 +303,7 @@ class CustomChildWrapper extends StatelessWidget { final HitTestBehavior? gestureDetectorBehavior; final bool? tightMode; final FilterQuality? filterQuality; + final bool? disableScaleGestures; final bool? disableGestures; final bool? enablePanAlways; @@ -330,6 +338,7 @@ class CustomChildWrapper extends StatelessWidget { tightMode: tightMode ?? false, filterQuality: filterQuality ?? FilterQuality.none, disableGestures: disableGestures ?? false, + disableScaleGestures: disableScaleGestures ?? false, enablePanAlways: enablePanAlways ?? false, ); } diff --git a/mobile/lib/widgets/photo_view/src/utils/ignorable_change_notifier.dart b/mobile/lib/widgets/photo_view/src/utils/ignorable_change_notifier.dart index d061b7b76c..da213903f6 100644 --- a/mobile/lib/widgets/photo_view/src/utils/ignorable_change_notifier.dart +++ b/mobile/lib/widgets/photo_view/src/utils/ignorable_change_notifier.dart @@ -8,8 +8,7 @@ import 'package:flutter/foundation.dart'; /// The common collection of listeners inherited from [ChangeNotifier] will be fired /// every time. class IgnorableChangeNotifier extends ChangeNotifier { - ObserverList? _ignorableListeners = - ObserverList(); + ObserverList? _ignorableListeners = ObserverList(); bool _debugAssertNotDisposed() { assert(() { @@ -51,8 +50,7 @@ class IgnorableChangeNotifier extends ChangeNotifier { void notifyListeners() { super.notifyListeners(); if (_ignorableListeners != null) { - final List localListeners = - List.from(_ignorableListeners!); + final List localListeners = List.from(_ignorableListeners!); for (VoidCallback listener in localListeners) { try { if (_ignorableListeners!.contains(listener)) { @@ -80,8 +78,7 @@ class IgnorableChangeNotifier extends ChangeNotifier { /// Just like [ValueNotifier] except it extends [IgnorableChangeNotifier] which has /// listeners that wont fire when [updateIgnoring] is called. -class IgnorableValueNotifier extends IgnorableChangeNotifier - implements ValueListenable { +class IgnorableValueNotifier extends IgnorableChangeNotifier implements ValueListenable { IgnorableValueNotifier(this._value); @override diff --git a/mobile/lib/widgets/photo_view/src/utils/photo_view_utils.dart b/mobile/lib/widgets/photo_view/src/utils/photo_view_utils.dart index facd701725..1efdc50161 100644 --- a/mobile/lib/widgets/photo_view/src/utils/photo_view_utils.dart +++ b/mobile/lib/widgets/photo_view/src/utils/photo_view_utils.dart @@ -101,11 +101,7 @@ class ScaleBoundaries { @override int get hashCode => - _minScale.hashCode ^ - _maxScale.hashCode ^ - _initialScale.hashCode ^ - outerSize.hashCode ^ - childSize.hashCode; + _minScale.hashCode ^ _maxScale.hashCode ^ _initialScale.hashCode ^ outerSize.hashCode ^ childSize.hashCode; } double _scaleForContained(Size size, Size childSize) { diff --git a/mobile/lib/widgets/search/curated_places_row.dart b/mobile/lib/widgets/search/curated_places_row.dart index 502b09bc4b..38092071d0 100644 --- a/mobile/lib/widgets/search/curated_places_row.dart +++ b/mobile/lib/widgets/search/curated_places_row.dart @@ -45,8 +45,7 @@ class CuratedPlacesRow extends StatelessWidget { } final actualIndex = index - actualContentIndex; final object = content[actualIndex]; - final thumbnailRequestUrl = - '${Store.get(StoreKey.serverEndpoint)}/assets/${object.id}/thumbnail'; + final thumbnailRequestUrl = '${Store.get(StoreKey.serverEndpoint)}/assets/${object.id}/thumbnail'; return SizedBox.square( dimension: imageSize, child: ThumbnailWithInfo( diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart index 76d6e80832..886f17b2cc 100644 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ b/mobile/lib/widgets/search/person_name_edit_form.dart @@ -9,7 +9,7 @@ class PersonNameEditFormResult { final bool success; final String updatedName; - PersonNameEditFormResult(this.success, this.updatedName); + const PersonNameEditFormResult(this.success, this.updatedName); } class PersonNameEditForm extends HookConsumerWidget { @@ -47,7 +47,7 @@ class PersonNameEditForm extends HookConsumerWidget { actions: [ TextButton( onPressed: () => context.pop( - PersonNameEditFormResult(false, ''), + const PersonNameEditFormResult(false, ''), ), child: Text( "cancel", diff --git a/mobile/lib/widgets/search/search_filter/common/dropdown.dart b/mobile/lib/widgets/search/search_filter/common/dropdown.dart index dd8785459f..dd0ec44e45 100644 --- a/mobile/lib/widgets/search/search_filter/common/dropdown.dart +++ b/mobile/lib/widgets/search/search_filter/common/dropdown.dart @@ -18,10 +18,10 @@ class SearchDropdown extends StatelessWidget { @override Widget build(BuildContext context) { - final menuStyle = MenuStyle( + final menuStyle = const MenuStyle( shape: WidgetStatePropertyAll( RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), + borderRadius: BorderRadius.all(Radius.circular(15)), ), ), ); diff --git a/mobile/lib/widgets/search/search_filter/location_picker.dart b/mobile/lib/widgets/search/search_filter/location_picker.dart index 499c1e6a50..eea4b52256 100644 --- a/mobile/lib/widgets/search/search_filter/location_picker.dart +++ b/mobile/lib/widgets/search/search_filter/location_picker.dart @@ -15,8 +15,7 @@ class LocationPicker extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final countryTextController = - useTextEditingController(text: filter?.country); + final countryTextController = useTextEditingController(text: filter?.country); final stateTextController = useTextEditingController(text: filter?.state); final cityTextController = useTextEditingController(text: filter?.city); diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index 44d01d274e..991664dd93 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -14,8 +14,8 @@ import 'package:immich_mobile/widgets/common/search_field.dart'; class PeoplePicker extends HookConsumerWidget { const PeoplePicker({super.key, required this.onSelect, this.filter}); - final Function(Set) onSelect; - final Set? filter; + final Function(Set) onSelect; + final Set? filter; @override Widget build(BuildContext context, WidgetRef ref) { @@ -24,7 +24,7 @@ class PeoplePicker extends HookConsumerWidget { final searchQuery = useState(''); final people = ref.watch(getAllPeopleProvider); final headers = ApiService.getRequestHeaders(); - final selectedPeople = useState>(filter ?? {}); + final selectedPeople = useState>(filter ?? {}); return Column( children: [ @@ -52,18 +52,14 @@ class PeoplePicker extends HookConsumerWidget { shrinkWrap: true, itemCount: people .where( - (person) => person.name - .toLowerCase() - .contains(searchQuery.value.toLowerCase()), + (person) => person.name.toLowerCase().contains(searchQuery.value.toLowerCase()), ) .length, padding: const EdgeInsets.all(8), itemBuilder: (context, index) { final person = people .where( - (person) => person.name - .toLowerCase() - .contains(searchQuery.value.toLowerCase()), + (person) => person.name.toLowerCase().contains(searchQuery.value.toLowerCase()), ) .toList()[index]; final isSelected = selectedPeople.value.contains(person); @@ -76,9 +72,7 @@ class PeoplePicker extends HookConsumerWidget { style: context.textTheme.bodyLarge?.copyWith( fontSize: 20, fontWeight: FontWeight.w500, - color: isSelected - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface, + color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface, ), ), leading: SizedBox( 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 c1e628adeb..60d1366fbd 100644 --- a/mobile/lib/widgets/search/search_filter/search_filter_chip.dart +++ b/mobile/lib/widgets/search/search_filter/search_filter_chip.dart @@ -27,8 +27,7 @@ class SearchFilterChip extends StatelessWidget { side: BorderSide(color: context.colorScheme.secondaryContainer), ), child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0), + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0), child: Row( children: [ Icon( diff --git a/mobile/lib/widgets/search/thumbnail_with_info.dart b/mobile/lib/widgets/search/thumbnail_with_info.dart index 8722bf8db8..23bdbc915b 100644 --- a/mobile/lib/widgets/search/thumbnail_with_info.dart +++ b/mobile/lib/widgets/search/thumbnail_with_info.dart @@ -22,8 +22,7 @@ class ThumbnailWithInfo extends StatelessWidget { @override Widget build(BuildContext context) { - var textAndIconColor = - context.isDarkTheme ? Colors.grey[100] : Colors.grey[700]; + var textAndIconColor = context.isDarkTheme ? Colors.grey[100] : Colors.grey[700]; return ThumbnailWithInfoContainer( onTap: onTap, borderRadius: borderRadius, @@ -37,8 +36,7 @@ class ThumbnailWithInfo extends StatelessWidget { fit: BoxFit.cover, imageUrl: imageUrl!, httpHeaders: ApiService.getRequestHeaders(), - errorWidget: (context, url, error) => - const Icon(Icons.image_not_supported_outlined), + errorWidget: (context, url, error) => const Icon(Icons.image_not_supported_outlined), ), ) : Center( diff --git a/mobile/lib/widgets/search/thumbnail_with_info_container.dart b/mobile/lib/widgets/search/thumbnail_with_info_container.dart index 1f5f3c2d16..e29d9e780c 100644 --- a/mobile/lib/widgets/search/thumbnail_with_info_container.dart +++ b/mobile/lib/widgets/search/thumbnail_with_info_container.dart @@ -43,9 +43,7 @@ class ThumbnailWithInfoContainer extends StatelessWidget { end: FractionalOffset.bottomCenter, colors: [ Colors.transparent, - label == '' - ? Colors.black.withValues(alpha: 0.1) - : Colors.black.withValues(alpha: 0.5), + label == '' ? Colors.black.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.5), ], stops: const [0.0, 1.0], ), @@ -53,8 +51,7 @@ class ThumbnailWithInfoContainer extends StatelessWidget { child: child, ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 8) + - const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(horizontal: 8) + const EdgeInsets.only(bottom: 8), child: Text( label, style: const TextStyle( diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index bd501ffcf7..3f569863de 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -25,23 +25,18 @@ class AdvancedSettings extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { bool isLoggedIn = ref.read(currentUserProvider) != null; - final advancedTroubleshooting = - useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); - final manageLocalMediaAndroid = - useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid); + final advancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); + final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid); final levelId = useAppSettingsState(AppSettingsEnum.logLevel); final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage); - final allowSelfSignedSSLCert = - useAppSettingsState(AppSettingsEnum.allowSelfSignedSSLCert); - final useAlternatePMFilter = - useAppSettingsState(AppSettingsEnum.photoManagerCustomFilter); + final allowSelfSignedSSLCert = useAppSettingsState(AppSettingsEnum.allowSelfSignedSSLCert); + final useAlternatePMFilter = useAppSettingsState(AppSettingsEnum.photoManagerCustomFilter); final logLevel = Level.LEVELS[levelId.value].name; useValueChanged( levelId.value, - (_, __) => - LogService.I.setLogLevel(Level.LEVELS[levelId.value].toLogLevel()), + (_, __) => LogService.I.setLogLevel(Level.LEVELS[levelId.value].toLogLevel()), ); Future checkAndroidVersion() async { @@ -72,9 +67,7 @@ class AdvancedSettings extends HookConsumerWidget { subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(), onChanged: (value) async { if (value) { - final result = await ref - .read(localFilesManagerRepositoryProvider) - .requestManageMediaPermission(); + final result = await ref.read(localFilesManagerRepositoryProvider).requestManageMediaPermission(); manageLocalMediaAndroid.value = result; } }, @@ -85,8 +78,7 @@ class AdvancedSettings extends HookConsumerWidget { }, ), SettingsSliderListTile( - text: "advanced_settings_log_level_title" - .tr(namedArgs: {'level': logLevel}), + text: "advanced_settings_log_level_title".tr(namedArgs: {'level': logLevel}), valueNotifier: levelId, maxValue: 8, minValue: 1, @@ -111,8 +103,7 @@ class AdvancedSettings extends HookConsumerWidget { SettingsSwitchListTile( valueNotifier: useAlternatePMFilter, title: "advanced_settings_enable_alternate_media_filter_title".tr(), - subtitle: - "advanced_settings_enable_alternate_media_filter_subtitle".tr(), + subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), ), ]; 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 72402c8d55..de8ae5c2b2 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 @@ -29,8 +29,7 @@ class LayoutSettings extends HookConsumerWidget { ), SettingsSliderListTile( valueNotifier: tilesPerRow, - text: 'theme_setting_asset_list_tiles_per_row_title' - .tr(namedArgs: {'count': "${tilesPerRow.value}"}), + text: 'theme_setting_asset_list_tiles_per_row_title'.tr(namedArgs: {'count': "${tilesPerRow.value}"}), label: "${tilesPerRow.value}", maxValue: 6, minValue: 2, diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart index cd12ea3eb2..550e3b5165 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart @@ -16,8 +16,7 @@ class AssetListSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final showStorageIndicator = - useAppSettingsState(AppSettingsEnum.storageIndicator); + final showStorageIndicator = useAppSettingsState(AppSettingsEnum.storageIndicator); final assetListSetting = [ SettingsSwitchListTile( diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart index 4534b6ee09..66f7e96943 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart @@ -15,8 +15,7 @@ class VideoViewerSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final useLoopVideo = useAppSettingsState(AppSettingsEnum.loopVideo); - final useOriginalVideo = - useAppSettingsState(AppSettingsEnum.loadOriginalVideo); + final useOriginalVideo = useAppSettingsState(AppSettingsEnum.loadOriginalVideo); return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/mobile/lib/widgets/settings/backup_settings/background_settings.dart b/mobile/lib/widgets/settings/backup_settings/background_settings.dart index d628309050..5207969045 100644 --- a/mobile/lib/widgets/settings/backup_settings/background_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/background_settings.dart @@ -19,8 +19,7 @@ class BackgroundBackupSettings extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isBackgroundEnabled = - ref.watch(backupProvider.select((s) => s.backgroundBackup)); + final isBackgroundEnabled = ref.watch(backupProvider.select((s) => s.backgroundBackup)); final iosSettings = ref.watch(iOSBackgroundSettingsProvider); void showErrorToUser(String msg) { @@ -80,12 +79,11 @@ class BackgroundBackupSettings extends ConsumerWidget { 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, - ), + onButtonTap: () => ref.read(backupProvider.notifier).configureBackgroundBackup( + enabled: true, + onError: showErrorToUser, + onBatteryInfo: showBatteryOptimizationInfoToUser, + ), ); } @@ -96,27 +94,23 @@ class BackgroundBackupSettings extends ConsumerWidget { onError: showErrorToUser, onBatteryInfo: showBatteryOptimizationInfoToUser, ), - if (Platform.isIOS && iosSettings?.appRefreshEnabled != true) - _IOSBackgroundRefreshDisabled(), - if (Platform.isIOS && iosSettings != null) - IosDebugInfoTile(settings: iosSettings), + 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(), + 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(), ); } @@ -133,8 +127,7 @@ class _BackgroundSettingsEnabled extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isWifiRequired = - ref.watch(backupProvider.select((s) => s.backupRequireWifi)); + final isWifiRequired = ref.watch(backupProvider.select((s) => s.backupRequireWifi)); final isWifiRequiredNotifier = useValueNotifier(isWifiRequired); useValueChanged( isWifiRequired, @@ -143,8 +136,7 @@ class _BackgroundSettingsEnabled extends HookConsumerWidget { ), ); - final isChargingRequired = - ref.watch(backupProvider.select((s) => s.backupRequireCharging)); + final isChargingRequired = ref.watch(backupProvider.select((s) => s.backupRequireCharging)); final isChargingRequiredNotifier = useValueNotifier(isChargingRequired); useValueChanged( isChargingRequired, @@ -160,22 +152,16 @@ class _BackgroundSettingsEnabled extends HookConsumerWidget { _ => 3, }; - int backupDelayToMilliseconds(int v) => - switch (v) { 0 => 5000, 1 => 30000, 2 => 120000, _ => 600000 }; + 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'}), + 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 backupTriggerDelay = ref.watch(backupProvider.select((s) => s.backupTriggerDelay)); final triggerDelay = useState(backupDelayToSliderValue(backupTriggerDelay)); useValueChanged( triggerDelay.value, @@ -191,35 +177,32 @@ class _BackgroundSettingsEnabled extends HookConsumerWidget { 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, - ), + 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, - ), + 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, - ), + onChanged: (enabled) => ref.read(backupProvider.notifier).configureBackgroundBackup( + requireCharging: enabled, + onError: onError, + onBatteryInfo: onBatteryInfo, + ), ), if (Platform.isAndroid) SettingsSliderListTile( diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart index 20f172cb28..59ea6f2105 100644 --- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart @@ -21,10 +21,8 @@ class BackupSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final ignoreIcloudAssets = - useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets); - final isAdvancedTroubleshooting = - useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); + final ignoreIcloudAssets = useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets); + final isAdvancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); final albumSync = useAppSettingsState(AppSettingsEnum.syncAlbums); final isCorruptCheckInProgress = ref.watch(backupVerificationProvider); final isAlbumSyncInProgress = useState(false); @@ -63,14 +61,10 @@ class BackupSettings extends HookConsumerWidget { ], ) : null, - subtileText: !isCorruptCheckInProgress - ? 'check_corrupt_asset_backup_description'.tr() - : 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) + ? () => ref.read(backupVerificationProvider.notifier).performBackupCheck(context) : null, ), if (albumSync.value) diff --git a/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart b/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart index fc3b32b203..a2ff00fe45 100644 --- a/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart @@ -12,8 +12,7 @@ class ForegroundBackupSettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isAutoBackup = ref.watch(backupProvider.select((s) => s.autoBackup)); - void onButtonTap() => - ref.read(backupProvider.notifier).setAutoBackup(!isAutoBackup); + void onButtonTap() => ref.read(backupProvider.notifier).setAutoBackup(!isAutoBackup); if (isAutoBackup) { return SettingsButtonListTile( diff --git a/mobile/lib/widgets/settings/beta_sync_settings/beta_sync_settings.dart b/mobile/lib/widgets/settings/beta_sync_settings/beta_sync_settings.dart new file mode 100644 index 0000000000..85da49357b --- /dev/null +++ b/mobile/lib/widgets/settings/beta_sync_settings/beta_sync_settings.dart @@ -0,0 +1,422 @@ +import 'dart:io'; + +import 'package:drift/drift.dart' as drift_db; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; +import 'package:immich_mobile/providers/sync_status.provider.dart'; +import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; + +class BetaSyncSettings extends HookConsumerWidget { + const BetaSyncSettings({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final assetService = ref.watch(assetServiceProvider); + final localAlbumService = ref.watch(localAlbumServiceProvider); + final remoteAlbumService = ref.watch(remoteAlbumServiceProvider); + final memoryService = ref.watch(driftMemoryServiceProvider); + + Future> loadCounts() async { + final assetCounts = assetService.getAssetCounts(); + final localAlbumCounts = localAlbumService.getCount(); + final remoteAlbumCounts = remoteAlbumService.getCount(); + final memoryCount = memoryService.getCount(); + final getLocalHashedCount = assetService.getLocalHashedCount(); + + return await Future.wait([ + assetCounts, + localAlbumCounts, + remoteAlbumCounts, + memoryCount, + getLocalHashedCount, + ]); + } + + Future resetDatabase() async { +// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94 + final drift = ref.read(driftProvider); + final database = drift.attachedDatabase; + await database.exclusively(() async { + // https://stackoverflow.com/a/65743498/25690041 + await database.customStatement('PRAGMA writable_schema = 1;'); + await database.customStatement('DELETE FROM sqlite_master;'); + await database.customStatement('VACUUM;'); + await database.customStatement('PRAGMA writable_schema = 0;'); + await database.customStatement('PRAGMA integrity_check'); + + await database.customStatement('PRAGMA user_version = 0'); + await database.beforeOpen( + // ignore: invalid_use_of_internal_member + database.resolvedEngine.executor, + drift_db.OpeningDetails(null, database.schemaVersion), + ); + await database.customStatement( + 'PRAGMA user_version = ${database.schemaVersion}', + ); + + // Refresh all stream queries + database.notifyUpdates({ + for (final table in database.allTables) drift_db.TableUpdate.onTable(table), + }); + }); + } + + Future exportDatabase() async { + try { + // WAL Checkpoint to ensure all changes are written to the database + await ref.read(driftProvider).customStatement("pragma wal_checkpoint(truncate)"); + final documentsDir = await getApplicationDocumentsDirectory(); + final dbFile = File(path.join(documentsDir.path, 'immich.sqlite')); + + if (!await dbFile.exists()) { + if (context.mounted) { + context.scaffoldMessenger.showSnackBar( + SnackBar( + content: Text("Database file not found".t(context: context)), + ), + ); + } + return; + } + + final timestamp = DateTime.now().millisecondsSinceEpoch; + final exportFile = File( + path.join( + documentsDir.path, + 'immich_export_$timestamp.sqlite', + ), + ); + + await dbFile.copy(exportFile.path); + + await Share.shareXFiles( + [XFile(exportFile.path)], + text: 'Immich Database Export', + ); + + Future.delayed(const Duration(seconds: 30), () async { + if (await exportFile.exists()) { + await exportFile.delete(); + } + }); + + if (context.mounted) { + context.scaffoldMessenger.showSnackBar( + SnackBar( + content: Text("Database exported successfully".t(context: context)), + ), + ); + } + } catch (e) { + if (context.mounted) { + context.scaffoldMessenger.showSnackBar( + SnackBar( + content: Text("Failed to export database: $e".t(context: context)), + ), + ); + } + } + } + + return FutureBuilder>( + future: loadCounts(), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const CircularProgressIndicator(); + } + + final assetCounts = snapshot.data![0]! as (int, int); + final localAssetCount = assetCounts.$1; + final remoteAssetCount = assetCounts.$2; + + final localAlbumCount = snapshot.data![1]! as int; + final remoteAlbumCount = snapshot.data![2]! as int; + final memoryCount = snapshot.data![3]! as int; + final localHashedCount = snapshot.data![4]! as int; + + return Padding( + padding: const EdgeInsets.only(top: 16, bottom: 32), + child: ListView( + children: [ + _SectionHeaderText(text: "assets".t(context: context)), + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Flex( + direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 8.0, + children: [ + Expanded( + child: EntitiyCountTile( + label: "local".t(context: context), + count: localAssetCount, + icon: Icons.smartphone, + ), + ), + Expanded( + child: EntitiyCountTile( + label: "remote".t(context: context), + count: remoteAssetCount, + icon: Icons.cloud, + ), + ), + ], + ), + ), + _SectionHeaderText(text: "albums".t(context: context)), + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Flex( + direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 8.0, + children: [ + Expanded( + child: EntitiyCountTile( + label: "local".t(context: context), + count: localAlbumCount, + icon: Icons.smartphone, + ), + ), + Expanded( + child: EntitiyCountTile( + label: "remote".t(context: context), + count: remoteAlbumCount, + icon: Icons.cloud, + ), + ), + ], + ), + ), + _SectionHeaderText(text: "other".t(context: context)), + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Flex( + direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 8.0, + children: [ + Expanded( + child: EntitiyCountTile( + label: "memories".t(context: context), + count: memoryCount, + icon: Icons.calendar_today, + ), + ), + Expanded( + child: EntitiyCountTile( + label: "hashed_assets".t(context: context), + count: localHashedCount, + icon: Icons.tag, + ), + ), + ], + ), + ), + const Divider( + height: 1, + indent: 16, + endIndent: 16, + ), + const SizedBox(height: 24), + _SectionHeaderText(text: "jobs".t(context: context)), + ListTile( + title: Text( + "sync_local".t(context: context), + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + "tap_to_run_job".t(context: context), + ), + leading: const Icon(Icons.sync), + trailing: _SyncStatusIcon( + status: ref.watch(syncStatusProvider).localSyncStatus, + ), + onTap: () { + ref.read(backgroundSyncProvider).syncLocal(full: true); + }, + ), + ListTile( + title: Text( + "sync_remote".t(context: context), + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + "tap_to_run_job".t(context: context), + ), + leading: const Icon(Icons.cloud_sync), + trailing: _SyncStatusIcon( + status: ref.watch(syncStatusProvider).remoteSyncStatus, + ), + onTap: () { + ref.read(backgroundSyncProvider).syncRemote(); + }, + ), + ListTile( + title: Text( + "hash_asset".t(context: context), + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + leading: const Icon(Icons.tag), + subtitle: Text( + "tap_to_run_job".t(context: context), + ), + trailing: _SyncStatusIcon( + status: ref.watch(syncStatusProvider).hashJobStatus, + ), + onTap: () { + ref.read(backgroundSyncProvider).hashAssets(); + }, + ), + const Divider( + height: 1, + indent: 16, + endIndent: 16, + ), + const SizedBox(height: 24), + _SectionHeaderText(text: "actions".t(context: context)), + ListTile( + title: Text( + "export_database".t(context: context), + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + "export_database_description".t(context: context), + ), + leading: const Icon(Icons.download), + onTap: exportDatabase, + ), + ListTile( + title: Text( + "reset_sqlite".t(context: context), + style: TextStyle( + color: context.colorScheme.error, + fontWeight: FontWeight.w500, + ), + ), + leading: Icon( + Icons.settings_backup_restore_rounded, + color: context.colorScheme.error, + ), + onTap: () async { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + "reset_sqlite".t(context: context), + ), + content: Text( + "reset_sqlite_confirmation".t(context: context), + ), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text("cancel".t(context: context)), + ), + TextButton( + onPressed: () async { + await resetDatabase(); + context.pop(); + context.scaffoldMessenger.showSnackBar( + SnackBar( + content: Text( + "reset_sqlite_success".t(context: context), + ), + ), + ); + }, + child: Text( + "confirm".t(context: context), + style: TextStyle( + color: context.colorScheme.error, + ), + ), + ), + ], + ); + }, + ); + }, + ), + ], + ), + ); + }, + ); + } +} + +class _SyncStatusIcon extends StatelessWidget { + final SyncStatus status; + + const _SyncStatusIcon({ + required this.status, + }); + + @override + Widget build(BuildContext context) { + return switch (status) { + SyncStatus.idle => const Icon( + Icons.pause_circle_outline_rounded, + ), + SyncStatus.syncing => const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + SyncStatus.success => const Icon( + Icons.check_circle_outline, + color: Colors.green, + ), + SyncStatus.error => Icon( + Icons.error_outline, + color: context.colorScheme.error, + ), + }; + } +} + +class _SectionHeaderText extends StatelessWidget { + final String text; + + const _SectionHeaderText({ + required this.text, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + text.toUpperCase(), + style: context.textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w500, + color: context.colorScheme.onSurface.withAlpha(200), + ), + ), + ); + } +} diff --git a/mobile/lib/widgets/settings/beta_sync_settings/entity_count_tile.dart b/mobile/lib/widgets/settings/beta_sync_settings/entity_count_tile.dart new file mode 100644 index 0000000000..2441e7d1ca --- /dev/null +++ b/mobile/lib/widgets/settings/beta_sync_settings/entity_count_tile.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; + +class EntitiyCountTile extends StatelessWidget { + final int count; + final String label; + final IconData icon; + + const EntitiyCountTile({ + super.key, + required this.count, + required this.label, + required this.icon, + }); + + String zeroPadding(int number, int targetWidth) { + final numStr = number.toString(); + return numStr.length < targetWidth ? "0" * (targetWidth - numStr.length) : ""; + } + + int calculateMaxDigits(double availableWidth) { + const double charWidth = 11.0; + return (availableWidth / charWidth).floor().clamp(1, 8); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainerLow, + borderRadius: const BorderRadius.all(Radius.circular(16)), + border: Border.all( + width: 0.5, + color: context.colorScheme.outline.withAlpha(25), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Icon and Label + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon( + icon, + color: context.primaryColor, + ), + const SizedBox(width: 8), + Text( + label, + style: TextStyle( + color: context.primaryColor, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + const SizedBox(height: 12), + // Number + LayoutBuilder( + builder: (context, constraints) { + final maxDigits = calculateMaxDigits(constraints.maxWidth); + return RichText( + text: TextSpan( + style: const TextStyle( + fontSize: 18, + fontFamily: 'OverpassMono', + fontWeight: FontWeight.w600, + ), + children: [ + TextSpan( + text: zeroPadding(count, maxDigits), + style: TextStyle( + color: context.colorScheme.onSurfaceSecondary.withAlpha(75), + ), + ), + TextSpan( + text: count.toString(), + style: TextStyle( + color: context.primaryColor, + ), + ), + ], + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart new file mode 100644 index 0000000000..f5f6d66898 --- /dev/null +++ b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart @@ -0,0 +1,273 @@ +import 'dart:math' as math; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; + +class BetaTimelineListTile extends ConsumerStatefulWidget { + const BetaTimelineListTile({ + super.key, + }); + + @override + ConsumerState createState() => _BetaTimelineListTileState(); +} + +class _BetaTimelineListTileState extends ConsumerState with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _rotationAnimation; + late Animation _pulseAnimation; + late Animation _gradientAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(seconds: 3), + vsync: this, + ); + + _rotationAnimation = Tween(begin: 0, end: 2 * math.pi).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.linear, + ), + ); + + _pulseAnimation = Tween(begin: 1, end: 1.1).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); + + _gradientAnimation = Tween(begin: 0, end: 1).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); + + _animationController.repeat(reverse: true); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.betaTimeline); + final serverInfo = ref.watch(serverInfoProvider); + final auth = ref.watch(authProvider); + + if (!auth.isAuthenticated || (serverInfo.serverVersion.minor < 136 && kReleaseMode)) { + return const SizedBox.shrink(); + } + + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + void onSwitchChanged(bool value) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: value ? const Text("Enable Beta Timeline") : const Text("Disable Beta Timeline"), + content: value + ? const Text( + "Are you sure you want to enable the beta timeline?", + ) + : const Text( + "Are you sure you want to disable the beta timeline?", + ), + actions: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: Text( + "cancel".t(context: context), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: context.colorScheme.outline, + ), + ), + ), + ElevatedButton( + onPressed: () async { + Navigator.of(context).pop(); + await ref.read(appSettingsServiceProvider).setSetting( + AppSettingsEnum.betaTimeline, + value, + ); + context.router.replaceAll( + [ChangeExperienceRoute(switchingToBeta: value)], + ); + }, + child: Text( + "ok".t(context: context), + ), + ), + ], + ); + }, + ); + } + + final gradientColors = [ + Color.lerp( + context.primaryColor.withValues(alpha: 0.5), + context.primaryColor.withValues(alpha: 0.3), + _gradientAnimation.value, + )!, + Color.lerp( + context.logoPink.withValues(alpha: 0.2), + context.logoPink.withValues(alpha: 0.4), + _gradientAnimation.value, + )!, + Color.lerp( + context.logoRed.withValues(alpha: 0.3), + context.logoRed.withValues(alpha: 0.5), + _gradientAnimation.value, + )!, + ]; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(12)), + gradient: LinearGradient( + colors: gradientColors, + stops: const [0.0, 0.5, 1.0], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + transform: GradientRotation(_rotationAnimation.value * 0.5), + ), + boxShadow: [ + BoxShadow( + color: context.primaryColor.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Container( + margin: const EdgeInsets.all(2), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(10.5)), + color: context.scaffoldBackgroundColor, + ), + child: Material( + borderRadius: const BorderRadius.all(Radius.circular(10.5)), + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(10.5)), + onTap: () => onSwitchChanged(!betaTimelineValue), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + children: [ + Transform.scale( + scale: _pulseAnimation.value, + child: Transform.rotate( + angle: _rotationAnimation.value * 0.02, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + context.primaryColor.withValues(alpha: 0.2), + context.primaryColor.withValues(alpha: 0.1), + ], + ), + ), + child: Icon( + Icons.auto_awesome, + color: context.primaryColor, + size: 20, + ), + ), + ), + ), + const SizedBox(width: 28), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "advanced_settings_beta_timeline_title".t(context: context), + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + gradient: LinearGradient( + colors: [ + context.primaryColor.withValues(alpha: 0.8), + context.primaryColor.withValues(alpha: 0.6), + ], + ), + ), + child: Text( + 'NEW', + style: context.textTheme.labelSmall?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 10, + height: 1.2, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + "advanced_settings_beta_timeline_subtitle".t(context: context), + style: context.textTheme.labelLarge?.copyWith( + color: context.textTheme.labelLarge?.color?.withValues(alpha: 0.9), + ), + maxLines: 2, + ), + ], + ), + ), + Switch.adaptive( + value: betaTimelineValue, + onChanged: onSwitchChanged, + activeColor: context.primaryColor, + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/widgets/settings/language_settings.dart b/mobile/lib/widgets/settings/language_settings.dart index 4d41d5b19b..012c966b60 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -31,13 +31,11 @@ class LanguageSettings extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final localeEntries = useMemoized(() => locales.entries.toList(), const []); final currentLocale = context.locale; - final filteredLocaleEntries = - useState>>(localeEntries); + final filteredLocaleEntries = useState>>(localeEntries); final selectedLocale = useState(currentLocale); final isLoading = useState(false); - final isButtonDisabled = - selectedLocale.value == currentLocale || isLoading.value; + final isButtonDisabled = selectedLocale.value == currentLocale || isLoading.value; final searchController = useTextEditingController(); final searchFocusNode = useFocusNode(); @@ -51,8 +49,7 @@ class LanguageSettings extends HookConsumerWidget { } else { filteredLocaleEntries.value = localeEntries .where( - (entry) => - entry.key.toLowerCase().contains(searchTerm.toLowerCase()), + (entry) => entry.key.toLowerCase().contains(searchTerm.toLowerCase()), ) .toList(); } @@ -94,12 +91,9 @@ class LanguageSettings extends HookConsumerWidget { itemExtent: 64.0, cacheExtent: 100, itemBuilder: (context, index) { - final countryName = - filteredLocaleEntries.value[index].key; - final localeValue = - filteredLocaleEntries.value[index].value; - final bool isSelected = - selectedLocale.value == localeValue; + final countryName = filteredLocaleEntries.value[index].key; + final localeValue = filteredLocaleEntries.value[index].value; + final bool isSelected = selectedLocale.value == localeValue; return _LanguageItem( key: ValueKey(localeValue.toString()), @@ -285,8 +279,7 @@ class _LanguageItem extends StatelessWidget { ), child: DecoratedBox( decoration: BoxDecoration( - color: - context.colorScheme.surfaceContainerLowest.withValues(alpha: .6), + color: context.colorScheme.surfaceContainerLowest.withValues(alpha: .6), borderRadius: const BorderRadius.all( Radius.circular(16.0), ), @@ -300,9 +293,7 @@ class _LanguageItem extends StatelessWidget { countryName, style: context.textTheme.titleSmall?.copyWith( fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: isSelected - ? context.colorScheme.primary - : context.colorScheme.onSurfaceVariant, + color: isSelected ? context.colorScheme.primary : context.colorScheme.onSurfaceVariant, ), ), trailing: isSelected diff --git a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart index 6302f9422a..ec0069f2d1 100644 --- a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart +++ b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart @@ -61,8 +61,7 @@ class EndpointInputState extends ConsumerState { final url = controller.text; setState(() => auxCheckStatus = AuxCheckStatus.loading); - final isValid = - await ref.read(authProvider.notifier).validateAuxilaryServerUrl(url); + final isValid = await ref.read(authProvider.notifier).validateAuxilaryServerUrl(url); setState(() { if (mounted) { @@ -140,8 +139,7 @@ class EndpointInputState extends ConsumerState { ), disabledBorder: OutlineInputBorder( borderSide: BorderSide( - color: - context.isDarkTheme ? Colors.grey[900]! : Colors.grey[300]!, + color: context.isDarkTheme ? Colors.grey[900]! : Colors.grey[300]!, ), borderRadius: const BorderRadius.all(Radius.circular(16)), ), diff --git a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart index 633d84c9c8..d0c212adf5 100644 --- a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -17,17 +17,15 @@ class ExternalNetworkPreference extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final entries = - useState([AuxilaryEndpoint(url: '', status: AuxCheckStatus.unknown)]); + final entries = useState( + [const AuxilaryEndpoint(url: '', status: AuxCheckStatus.unknown)], + ); final canSave = useState(false); saveEndpointList() { - canSave.value = - entries.value.every((e) => e.status == AuxCheckStatus.valid); + canSave.value = entries.value.every((e) => e.status == AuxCheckStatus.valid); - final endpointList = entries.value - .where((url) => url.status == AuxCheckStatus.valid) - .toList(); + final endpointList = entries.value.where((url) => url.status == AuxCheckStatus.valid).toList(); final jsonString = jsonEncode(endpointList); @@ -38,8 +36,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { } updateValidationStatus(String url, int index, AuxCheckStatus status) { - entries.value[index] = - entries.value[index].copyWith(url: url, status: status); + entries.value[index] = entries.value[index].copyWith(url: url, status: status); saveEndpointList(); } @@ -89,8 +86,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { } final List jsonList = jsonDecode(jsonString); - entries.value = - jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList(); + entries.value = jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList(); return null; }, const [], @@ -169,7 +165,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { ? () { entries.value = [ ...entries.value, - AuxilaryEndpoint( + const AuxilaryEndpoint( url: '', status: AuxCheckStatus.unknown, ), diff --git a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart index a50d216a9d..ac61019aaf 100644 --- a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart @@ -59,8 +59,7 @@ class LocalNetworkPreference extends HookConsumerWidget { useEffect( () { final wifiName = ref.read(authProvider.notifier).getSavedWifiName(); - final localEndpoint = - ref.read(authProvider.notifier).getSavedLocalEndpoint(); + final localEndpoint = ref.read(authProvider.notifier).getSavedLocalEndpoint(); if (wifiName != null) { wifiNameText.value = wifiName; @@ -131,8 +130,7 @@ class LocalNetworkPreference extends HookConsumerWidget { saveWifiName(wifiName); } - final serverEndpoint = - ref.read(authProvider.notifier).getServerEndpoint(); + final serverEndpoint = ref.read(authProvider.notifier).getServerEndpoint(); if (serverEndpoint != null) { saveLocalEndpoint(serverEndpoint); @@ -194,10 +192,7 @@ class LocalNetworkPreference extends HookConsumerWidget { wifiNameText.value, style: context.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.bold, - color: enabled - ? context.primaryColor - : context.colorScheme.onSurface - .withAlpha(100), + color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100), fontFamily: 'Inconsolata', ), ), @@ -217,10 +212,7 @@ class LocalNetworkPreference extends HookConsumerWidget { localEndpointText.value, style: context.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.bold, - color: enabled - ? context.primaryColor - : context.colorScheme.onSurface - .withAlpha(100), + color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100), fontFamily: 'Inconsolata', ), ), @@ -238,8 +230,7 @@ class LocalNetworkPreference extends HookConsumerWidget { height: 48, child: OutlinedButton.icon( icon: const Icon(Icons.wifi_find_rounded), - label: - Text('use_current_connection'.tr().toUpperCase()), + label: Text('use_current_connection'.tr().toUpperCase()), onPressed: enabled ? autofillCurrentNetwork : null, ), ), diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart index 587a0ce6d3..24d62b2663 100644 --- a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -18,8 +18,7 @@ class NetworkingSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final currentEndpoint = getServerUrl(); - final featureEnabled = - useAppSettingsState(AppSettingsEnum.autoEndpointSwitching); + final featureEnabled = useAppSettingsState(AppSettingsEnum.autoEndpointSwitching); Future checkWifiReadPermission() async { final [hasLocationInUse, hasLocationAlways] = await Future.wait([ @@ -39,9 +38,7 @@ class NetworkingSettings extends HookConsumerWidget { actions: [ TextButton( onPressed: () async { - final isGrant = await ref - .read(networkProvider.notifier) - .requestWifiReadPermission(); + final isGrant = await ref.read(networkProvider.notifier).requestWifiReadPermission(); Navigator.pop(context, isGrant); }, @@ -63,9 +60,7 @@ class NetworkingSettings extends HookConsumerWidget { actions: [ TextButton( onPressed: () async { - final isGrant = await ref - .read(networkProvider.notifier) - .requestWifiReadBackgroundPermission(); + final isGrant = await ref.read(networkProvider.notifier).requestWifiReadBackgroundPermission(); Navigator.pop(context, isGrant); }, @@ -77,8 +72,7 @@ class NetworkingSettings extends HookConsumerWidget { ); } - if (isGrantLocationAlwaysPermission != null && - !isGrantLocationAlwaysPermission) { + if (isGrantLocationAlwaysPermission != null && !isGrantLocationAlwaysPermission) { await ref.read(networkProvider.notifier).openSettings(); } } @@ -101,9 +95,7 @@ class NetworkingSettings extends HookConsumerWidget { padding: const EdgeInsets.only(top: 8, left: 16, bottom: 8), child: NetworkPreferenceTitle( title: "current_server_address".tr().toUpperCase(), - icon: (currentEndpoint?.startsWith('https') ?? false) - ? Icons.https_outlined - : Icons.http_outlined, + icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined, ), ), Padding( diff --git a/mobile/lib/widgets/settings/notification_setting.dart b/mobile/lib/widgets/settings/notification_setting.dart index cf6745199e..f4e520f4df 100644 --- a/mobile/lib/widgets/settings/notification_setting.dart +++ b/mobile/lib/widgets/settings/notification_setting.dart @@ -20,12 +20,9 @@ class NotificationSetting extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final permissionService = ref.watch(notificationPermissionProvider); - final sliderValue = - useAppSettingsState(AppSettingsEnum.uploadErrorNotificationGracePeriod); - final totalProgressValue = - useAppSettingsState(AppSettingsEnum.backgroundBackupTotalProgress); - final singleProgressValue = - useAppSettingsState(AppSettingsEnum.backgroundBackupSingleProgress); + final sliderValue = useAppSettingsState(AppSettingsEnum.uploadErrorNotificationGracePeriod); + final totalProgressValue = useAppSettingsState(AppSettingsEnum.backgroundBackupTotalProgress); + final singleProgressValue = useAppSettingsState(AppSettingsEnum.backgroundBackupSingleProgress); final hasPermission = permissionService == PermissionStatus.granted; @@ -55,8 +52,7 @@ class NotificationSetting extends HookConsumerWidget { ); } - final String formattedValue = - _formatSliderValue(sliderValue.value.toDouble()); + final String formattedValue = _formatSliderValue(sliderValue.value.toDouble()); final notificationSettings = [ if (!hasPermission) @@ -65,10 +61,8 @@ class NotificationSetting extends HookConsumerWidget { title: 'notification_permission_list_tile_title'.tr(), subtileText: 'notification_permission_list_tile_content'.tr(), buttonText: 'notification_permission_list_tile_enable_button'.tr(), - onButtonTap: () => ref - .watch(notificationPermissionProvider.notifier) - .requestNotificationPermission() - .then((permission) { + onButtonTap: () => + ref.watch(notificationPermissionProvider.notifier).requestNotificationPermission().then((permission) { if (permission == PermissionStatus.permanentlyDenied) { showPermissionsDialog(); } @@ -89,8 +83,7 @@ class NotificationSetting extends HookConsumerWidget { SettingsSliderListTile( enabled: hasPermission, valueNotifier: sliderValue, - text: 'setting_notifications_notify_failures_grace_period' - .tr(namedArgs: {'duration': formattedValue}), + text: 'setting_notifications_notify_failures_grace_period'.tr(namedArgs: {'duration': formattedValue}), maxValue: 5.0, noDivisons: 5, label: formattedValue, @@ -105,8 +98,7 @@ String _formatSliderValue(double v) { if (v == 0.0) { return 'setting_notifications_notify_immediately'.tr(); } else if (v == 1.0) { - return 'setting_notifications_notify_minutes' - .tr(namedArgs: {'count': '30'}); + return 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '30'}); } else if (v == 2.0) { return 'setting_notifications_notify_hours'.tr(namedArgs: {'count': '2'}); } else if (v == 3.0) { diff --git a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart index 90a123bfbd..fbd94b68d6 100644 --- a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart @@ -14,10 +14,8 @@ class HapticSetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final hapticFeedbackSetting = - useAppSettingsState(AppSettingsEnum.enableHapticFeedback); - final isHapticFeedbackEnabled = - useValueNotifier(hapticFeedbackSetting.value); + final hapticFeedbackSetting = useAppSettingsState(AppSettingsEnum.enableHapticFeedback); + final isHapticFeedbackEnabled = useValueNotifier(hapticFeedbackSetting.value); onHapticFeedbackChange(bool isEnabled) { hapticFeedbackSetting.value = isEnabled; diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index af34ab9e16..b4f70c5b9b 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -7,9 +7,9 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/theme.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/theme/color_scheme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; class PrimaryColorSetting extends HookConsumerWidget { const PrimaryColorSetting({ @@ -20,18 +20,15 @@ class PrimaryColorSetting extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final themeProvider = ref.read(immichThemeProvider); - final primaryColorSetting = - useAppSettingsState(AppSettingsEnum.primaryColor); - final systemPrimaryColorSetting = - useAppSettingsState(AppSettingsEnum.dynamicTheme); + final primaryColorSetting = useAppSettingsState(AppSettingsEnum.primaryColor); + final systemPrimaryColorSetting = useAppSettingsState(AppSettingsEnum.dynamicTheme); final currentPreset = useValueNotifier(ref.read(immichThemePresetProvider)); const tileSize = 55.0; useValueChanged( primaryColorSetting.value, - (_, __) => currentPreset.value = ImmichColorPreset.values - .firstWhere((e) => e.name == primaryColorSetting.value), + (_, __) => currentPreset.value = ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorSetting.value), ); void popBottomSheet() { @@ -131,13 +128,12 @@ class PrimaryColorSetting extends HookConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 20), margin: const EdgeInsets.only(top: 10), child: SwitchListTile.adaptive( - contentPadding: - const EdgeInsets.symmetric(vertical: 6, horizontal: 20), + contentPadding: const EdgeInsets.symmetric(vertical: 6, horizontal: 20), dense: true, activeColor: context.primaryColor, tileColor: context.colorScheme.surfaceContainerHigh, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(15)), ), title: Text( 'theme_setting_system_primary_color_title'.tr(), @@ -164,8 +160,7 @@ class PrimaryColorSetting extends HookConsumerWidget { topColor: theme.light.primary, bottomColor: theme.dark.primary, tileSize: tileSize, - showSelector: currentPreset.value == preset && - !systemPrimaryColorSetting.value, + showSelector: currentPreset.value == preset && !systemPrimaryColorSetting.value, ), ); }).toList(), @@ -201,8 +196,7 @@ class PrimaryColorSetting extends HookConsumerWidget { ), Text( "theme_setting_primary_color_subtitle".tr(), - style: context.textTheme.bodyMedium - ?.copyWith(color: context.colorScheme.onSurfaceSecondary), + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ], ), diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index 0c68de4c52..6d5a50e730 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -20,13 +20,10 @@ class ThemeSetting extends HookConsumerWidget { final currentThemeString = useAppSettingsState(AppSettingsEnum.themeMode); final currentTheme = useValueNotifier(ref.read(immichThemeModeProvider)); final isDarkTheme = useValueNotifier(currentTheme.value == ThemeMode.dark); - final isSystemTheme = - useValueNotifier(currentTheme.value == ThemeMode.system); + final isSystemTheme = useValueNotifier(currentTheme.value == ThemeMode.system); - final applyThemeToBackgroundSetting = - useAppSettingsState(AppSettingsEnum.colorfulInterface); - final applyThemeToBackgroundProvider = - useValueNotifier(ref.read(colorfulInterfaceSettingProvider)); + final applyThemeToBackgroundSetting = useAppSettingsState(AppSettingsEnum.colorfulInterface); + final applyThemeToBackgroundProvider = useValueNotifier(ref.read(colorfulInterfaceSettingProvider)); useValueChanged( currentThemeString.value, @@ -39,8 +36,7 @@ class ThemeSetting extends HookConsumerWidget { useValueChanged( applyThemeToBackgroundSetting.value, - (_, __) => applyThemeToBackgroundProvider.value = - applyThemeToBackgroundSetting.value, + (_, __) => applyThemeToBackgroundProvider.value = applyThemeToBackgroundSetting.value, ); void onThemeChange(bool isDark) { @@ -74,8 +70,7 @@ class ThemeSetting extends HookConsumerWidget { void onSurfaceColorSettingChange(bool useColorfulInterface) { applyThemeToBackgroundSetting.value = useColorfulInterface; - ref.watch(colorfulInterfaceSettingProvider.notifier).state = - useColorfulInterface; + ref.watch(colorfulInterfaceSettingProvider.notifier).state = useColorfulInterface; } return Column( diff --git a/mobile/lib/widgets/settings/settings_button_list_tile.dart b/mobile/lib/widgets/settings/settings_button_list_tile.dart index c8bd8e4b58..602bbd75cf 100644 --- a/mobile/lib/widgets/settings/settings_button_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_button_list_tile.dart @@ -50,8 +50,7 @@ class SettingsButtonListTile extends StatelessWidget { ), if (subtitle != null) subtitle!, const SizedBox(height: 6), - child ?? - ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)), + child ?? ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)), ], ), ); diff --git a/mobile/lib/widgets/settings/settings_card.dart b/mobile/lib/widgets/settings/settings_card.dart new file mode 100644 index 0000000000..523add9690 --- /dev/null +++ b/mobile/lib/widgets/settings/settings_card.dart @@ -0,0 +1,61 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class SettingsCard extends StatelessWidget { + const SettingsCard({ + super.key, + required this.icon, + required this.title, + required this.subtitle, + required this.settingRoute, + }); + + final IconData icon; + final String title; + final String subtitle; + final PageRouteInfo settingRoute; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: Card( + elevation: 0, + clipBehavior: Clip.antiAlias, + color: context.colorScheme.surfaceContainer, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + margin: const EdgeInsets.symmetric(vertical: 4.0), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + leading: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(16)), + color: context.isDarkTheme ? Colors.black26 : Colors.white.withAlpha(100), + ), + padding: const EdgeInsets.all(16.0), + child: Icon(icon, color: context.primaryColor), + ), + title: Text( + title, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ), + subtitle: Text( + subtitle, + style: context.textTheme.labelLarge, + ), + onTap: () => context.pushRoute(settingRoute), + ), + ), + ); + } +} diff --git a/mobile/lib/widgets/settings/settings_radio_list_tile.dart b/mobile/lib/widgets/settings/settings_radio_list_tile.dart index 1c26682a65..3f3a6cbe69 100644 --- a/mobile/lib/widgets/settings/settings_radio_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_radio_list_tile.dart @@ -5,7 +5,7 @@ class SettingsRadioGroup { final String title; final T value; - SettingsRadioGroup({required this.title, required this.value}); + const SettingsRadioGroup({required this.title, required this.value}); } class SettingsRadioListTile extends StatelessWidget { diff --git a/mobile/lib/widgets/settings/settings_switch_list_tile.dart b/mobile/lib/widgets/settings/settings_switch_list_tile.dart index 8aa4ec0a60..456acd83cd 100644 --- a/mobile/lib/widgets/settings/settings_switch_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_switch_list_tile.dart @@ -40,8 +40,7 @@ class SettingsSwitchListTile extends StatelessWidget { selectedTileColor: enabled ? null : context.themeData.disabledColor, value: valueNotifier.value, onChanged: onSwitchChanged, - activeColor: - enabled ? context.primaryColor : context.themeData.disabledColor, + activeColor: enabled ? context.primaryColor : context.themeData.disabledColor, dense: true, secondary: icon != null ? Icon( @@ -63,9 +62,7 @@ class SettingsSwitchListTile extends StatelessWidget { subtitle!, style: subtitleStyle ?? context.textTheme.bodyMedium?.copyWith( - color: enabled - ? context.colorScheme.onSurfaceSecondary - : context.themeData.disabledColor, + color: enabled ? context.colorScheme.onSurfaceSecondary : context.themeData.disabledColor, ), ) : null, diff --git a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart index 6fdbb156d9..ae5b065294 100644 --- a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart +++ b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart @@ -20,8 +20,7 @@ class SslClientCertSettings extends StatefulWidget { } class _SslClientCertSettingsState extends State { - _SslClientCertSettingsState() - : isCertExist = SSLClientCertStoreVal.load() != null; + _SslClientCertSettingsState() : isCertExist = SSLClientCertStoreVal.load() != null; bool isCertExist; @@ -62,9 +61,7 @@ class _SslClientCertSettingsState extends State { width: 15, ), ElevatedButton( - onPressed: widget.isLoggedIn || !isCertExist - ? null - : () => removeCert(context), + onPressed: widget.isLoggedIn || !isCertExist ? null : () async => await removeCert(context), child: Text("remove".tr()), ), ], @@ -89,7 +86,11 @@ class _SslClientCertSettingsState extends State { ); } - void storeCert(BuildContext context, Uint8List data, String? password) { + Future storeCert( + BuildContext context, + Uint8List data, + String? password, + ) async { if (password != null && password.isEmpty) { password = null; } @@ -103,7 +104,7 @@ class _SslClientCertSettingsState extends State { showMessage(context, "client_cert_invalid_msg".tr()); return; } - cert.save(); + await cert.save(); HttpSSLOptions.apply(); setState( () => isCertExist = true, @@ -127,8 +128,7 @@ class _SslClientCertSettingsState extends State { ), actions: [ TextButton( - onPressed: () => - {ctx.pop(), storeCert(context, data, password.text)}, + onPressed: () async => {ctx.pop(), await storeCert(context, data, password.text)}, child: Text("client_cert_dialog_msg_confirm".tr()), ), ], @@ -151,8 +151,8 @@ class _SslClientCertSettingsState extends State { } } - void removeCert(BuildContext context) { - SSLClientCertStoreVal.delete(); + Future removeCert(BuildContext context) async { + await SSLClientCertStoreVal.delete(); HttpSSLOptions.apply(); setState( () => isCertExist = false, diff --git a/mobile/lib/widgets/shared_link/shared_link_item.dart b/mobile/lib/widgets/shared_link/shared_link_item.dart index 69e763ea09..82194d2c7c 100644 --- a/mobile/lib/widgets/shared_link/shared_link_item.dart +++ b/mobile/lib/widgets/shared_link/shared_link_item.dart @@ -45,17 +45,13 @@ class SharedLinkItem extends ConsumerWidget { if (difference.inHours % 24 > 12) { dayDifference += 1; } - expiresText = "shared_link_expires_days" - .tr(namedArgs: {'count': dayDifference.toString()}); + expiresText = "shared_link_expires_days".tr(namedArgs: {'count': dayDifference.toString()}); } else if (difference.inHours > 0) { - expiresText = "shared_link_expires_hours" - .tr(namedArgs: {'count': difference.inHours.toString()}); + expiresText = "shared_link_expires_hours".tr(namedArgs: {'count': difference.inHours.toString()}); } else if (difference.inMinutes > 0) { - expiresText = "shared_link_expires_minutes" - .tr(namedArgs: {'count': difference.inMinutes.toString()}); + expiresText = "shared_link_expires_minutes".tr(namedArgs: {'count': difference.inMinutes.toString()}); } else if (difference.inSeconds > 0) { - expiresText = "shared_link_expires_seconds" - .tr(namedArgs: {'count': difference.inSeconds.toString()}); + expiresText = "shared_link_expires_seconds".tr(namedArgs: {'count': difference.inSeconds.toString()}); } } return Text( @@ -68,17 +64,14 @@ class SharedLinkItem extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final colorScheme = context.colorScheme; final isDarkMode = colorScheme.brightness == Brightness.dark; - final thumbnailUrl = sharedLink.thumbAssetId != null - ? getThumbnailUrlForRemoteId(sharedLink.thumbAssetId!) - : null; + final thumbnailUrl = sharedLink.thumbAssetId != null ? getThumbnailUrlForRemoteId(sharedLink.thumbAssetId!) : null; final imageSize = math.min(context.width / 4, 100.0); void copyShareLinkToClipboard() { final externalDomain = ref.read( serverInfoProvider.select((s) => s.serverConfig.externalDomain), ); - var serverUrl = - externalDomain.isNotEmpty ? externalDomain : getServerUrl(); + var serverUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl(); if (serverUrl != null && !serverUrl.endsWith('/')) { serverUrl += '/'; } @@ -116,9 +109,7 @@ class SharedLinkItem extends ConsumerWidget { return ConfirmDialog( title: "delete_shared_link_dialog_title", content: "confirm_delete_shared_link", - onOk: () => ref - .read(sharedLinksStateProvider.notifier) - .deleteLink(sharedLink.id), + onOk: () => ref.read(sharedLinksStateProvider.notifier).deleteLink(sharedLink.id), ); }, ); @@ -181,8 +172,7 @@ class SharedLinkItem extends ConsumerWidget { children: [ if (sharedLink.allowUpload) buildInfoChip("upload".tr()), if (sharedLink.allowDownload) buildInfoChip("download".tr()), - if (sharedLink.showMetadata) - buildInfoChip("shared_link_info_chip_metadata".tr()), + if (sharedLink.showMetadata) buildInfoChip("shared_link_info_chip_metadata".tr()), ], ); } @@ -197,8 +187,7 @@ class SharedLinkItem extends ConsumerWidget { iconSize: actionIconSize, icon: const Icon(Icons.delete_outline), style: const ButtonStyle( - tapTargetSize: - MaterialTapTargetSize.shrinkWrap, // the '2023' part + tapTargetSize: MaterialTapTargetSize.shrinkWrap, // the '2023' part ), onPressed: deleteShareLink, ), @@ -208,11 +197,9 @@ class SharedLinkItem extends ConsumerWidget { iconSize: actionIconSize, icon: const Icon(Icons.edit_outlined), style: const ButtonStyle( - tapTargetSize: - MaterialTapTargetSize.shrinkWrap, // the '2023' part + tapTargetSize: MaterialTapTargetSize.shrinkWrap, // the '2023' part ), - onPressed: () => context - .pushRoute(SharedLinkEditRoute(existingLink: sharedLink)), + onPressed: () => context.pushRoute(SharedLinkEditRoute(existingLink: sharedLink)), ), IconButton( splashRadius: 25, @@ -220,8 +207,7 @@ class SharedLinkItem extends ConsumerWidget { iconSize: actionIconSize, icon: const Icon(Icons.copy_outlined), style: const ButtonStyle( - tapTargetSize: - MaterialTapTargetSize.shrinkWrap, // the '2023' part + tapTargetSize: MaterialTapTargetSize.shrinkWrap, // the '2023' part ), onPressed: copyShareLinkToClipboard, ), @@ -240,7 +226,7 @@ class SharedLinkItem extends ConsumerWidget { verticalOffset: 0, decoration: BoxDecoration( color: colorScheme.primary.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(10), + borderRadius: const BorderRadius.all(Radius.circular(10)), ), textStyle: TextStyle( color: isDarkMode ? Colors.black : Colors.white, @@ -268,7 +254,7 @@ class SharedLinkItem extends ConsumerWidget { verticalOffset: 0, decoration: BoxDecoration( color: colorScheme.primary.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(10), + borderRadius: const BorderRadius.all(Radius.circular(10)), ), textStyle: TextStyle( color: isDarkMode ? Colors.black : Colors.white, diff --git a/mobile/makefile b/mobile/makefile index 64992ec946..356649d5dd 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -1,4 +1,4 @@ -.PHONY: build watch create_app_icon create_splash build_release_android pigeon +.PHONY: build watch create_app_icon create_splash build_release_android pigeon test analyze format build: dart run build_runner build --delete-conflicting-outputs @@ -28,4 +28,15 @@ translation: dart run easy_localization:generate -S ../i18n dart run bin/generate_keys.dart dart format lib/generated/codegen_loader.g.dart - dart format lib/generated/intl_keys.g.dart \ No newline at end of file + dart format lib/generated/intl_keys.g.dart + +analyze: + dart analyze --fatal-infos + dcm analyze lib --fatal-style --fatal-warnings + +format: +# Ignore generated files manually until https://github.com/dart-lang/dart_style/issues/864 is resolved + dart format --set-exit-if-changed $$(find lib -name '*.dart' -not \( -name 'generated_plugin_registrant.dart' -o -name '*.g.dart' -o -name '*.drift.dart' \)) + +test: + flutter test diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index a412c237dd..3181b03a47 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.135.3 +- API version: 1.136.0 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -169,6 +169,8 @@ Class | Method | HTTP request | Description *PartnersApi* | [**removePartner**](doc//PartnersApi.md#removepartner) | **DELETE** /partners/{id} | *PartnersApi* | [**updatePartner**](doc//PartnersApi.md#updatepartner) | **PUT** /partners/{id} | *PeopleApi* | [**createPerson**](doc//PeopleApi.md#createperson) | **POST** /people | +*PeopleApi* | [**deletePeople**](doc//PeopleApi.md#deletepeople) | **DELETE** /people | +*PeopleApi* | [**deletePerson**](doc//PeopleApi.md#deleteperson) | **DELETE** /people/{id} | *PeopleApi* | [**getAllPeople**](doc//PeopleApi.md#getallpeople) | **GET** /people | *PeopleApi* | [**getPerson**](doc//PeopleApi.md#getperson) | **GET** /people/{id} | *PeopleApi* | [**getPersonStatistics**](doc//PeopleApi.md#getpersonstatistics) | **GET** /people/{id}/statistics | @@ -206,6 +208,7 @@ Class | Method | HTTP request | Description *SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} | *SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions | *SessionsApi* | [**lockSession**](doc//SessionsApi.md#locksession) | **POST** /sessions/{id}/lock | +*SessionsApi* | [**updateSession**](doc//SessionsApi.md#updatesession) | **PUT** /sessions/{id} | *SharedLinksApi* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets | *SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links | *SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links | @@ -218,6 +221,7 @@ Class | Method | HTTP request | Description *StacksApi* | [**deleteStack**](doc//StacksApi.md#deletestack) | **DELETE** /stacks/{id} | *StacksApi* | [**deleteStacks**](doc//StacksApi.md#deletestacks) | **DELETE** /stacks | *StacksApi* | [**getStack**](doc//StacksApi.md#getstack) | **GET** /stacks/{id} | +*StacksApi* | [**removeAssetFromStack**](doc//StacksApi.md#removeassetfromstack) | **DELETE** /stacks/{id}/assets/{assetId} | *StacksApi* | [**searchStacks**](doc//StacksApi.md#searchstacks) | **GET** /stacks | *StacksApi* | [**updateStack**](doc//StacksApi.md#updatestack) | **PUT** /stacks/{id} | *SyncApi* | [**deleteSyncAck**](doc//SyncApi.md#deletesyncack) | **DELETE** /sync/ack | @@ -447,6 +451,7 @@ Class | Method | HTTP request | Description - [SessionCreateResponseDto](doc//SessionCreateResponseDto.md) - [SessionResponseDto](doc//SessionResponseDto.md) - [SessionUnlockDto](doc//SessionUnlockDto.md) + - [SessionUpdateDto](doc//SessionUpdateDto.md) - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) - [SharedLinkEditDto](doc//SharedLinkEditDto.md) - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) @@ -464,18 +469,33 @@ Class | Method | HTTP request | Description - [SyncAckDto](doc//SyncAckDto.md) - [SyncAckSetDto](doc//SyncAckSetDto.md) - [SyncAlbumDeleteV1](doc//SyncAlbumDeleteV1.md) + - [SyncAlbumToAssetDeleteV1](doc//SyncAlbumToAssetDeleteV1.md) + - [SyncAlbumToAssetV1](doc//SyncAlbumToAssetV1.md) - [SyncAlbumUserDeleteV1](doc//SyncAlbumUserDeleteV1.md) - [SyncAlbumUserV1](doc//SyncAlbumUserV1.md) - [SyncAlbumV1](doc//SyncAlbumV1.md) - [SyncAssetDeleteV1](doc//SyncAssetDeleteV1.md) - [SyncAssetExifV1](doc//SyncAssetExifV1.md) + - [SyncAssetFaceDeleteV1](doc//SyncAssetFaceDeleteV1.md) + - [SyncAssetFaceV1](doc//SyncAssetFaceV1.md) - [SyncAssetV1](doc//SyncAssetV1.md) + - [SyncAuthUserV1](doc//SyncAuthUserV1.md) - [SyncEntityType](doc//SyncEntityType.md) + - [SyncMemoryAssetDeleteV1](doc//SyncMemoryAssetDeleteV1.md) + - [SyncMemoryAssetV1](doc//SyncMemoryAssetV1.md) + - [SyncMemoryDeleteV1](doc//SyncMemoryDeleteV1.md) + - [SyncMemoryV1](doc//SyncMemoryV1.md) - [SyncPartnerDeleteV1](doc//SyncPartnerDeleteV1.md) - [SyncPartnerV1](doc//SyncPartnerV1.md) + - [SyncPersonDeleteV1](doc//SyncPersonDeleteV1.md) + - [SyncPersonV1](doc//SyncPersonV1.md) - [SyncRequestType](doc//SyncRequestType.md) + - [SyncStackDeleteV1](doc//SyncStackDeleteV1.md) + - [SyncStackV1](doc//SyncStackV1.md) - [SyncStreamDto](doc//SyncStreamDto.md) - [SyncUserDeleteV1](doc//SyncUserDeleteV1.md) + - [SyncUserMetadataDeleteV1](doc//SyncUserMetadataDeleteV1.md) + - [SyncUserMetadataV1](doc//SyncUserMetadataV1.md) - [SyncUserV1](doc//SyncUserV1.md) - [SystemConfigBackupsDto](doc//SystemConfigBackupsDto.md) - [SystemConfigDto](doc//SystemConfigDto.md) @@ -493,6 +513,7 @@ Class | Method | HTTP request | Description - [SystemConfigMapDto](doc//SystemConfigMapDto.md) - [SystemConfigMetadataDto](doc//SystemConfigMetadataDto.md) - [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md) + - [SystemConfigNightlyTasksDto](doc//SystemConfigNightlyTasksDto.md) - [SystemConfigNotificationsDto](doc//SystemConfigNotificationsDto.md) - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md) - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md) @@ -536,6 +557,7 @@ Class | Method | HTTP request | Description - [UserAdminUpdateDto](doc//UserAdminUpdateDto.md) - [UserAvatarColor](doc//UserAvatarColor.md) - [UserLicense](doc//UserLicense.md) + - [UserMetadataKey](doc//UserMetadataKey.md) - [UserPreferencesResponseDto](doc//UserPreferencesResponseDto.md) - [UserPreferencesUpdateDto](doc//UserPreferencesUpdateDto.md) - [UserResponseDto](doc//UserResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 573081503f..8c1fa1a80a 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -232,6 +232,7 @@ part 'model/session_create_dto.dart'; part 'model/session_create_response_dto.dart'; part 'model/session_response_dto.dart'; part 'model/session_unlock_dto.dart'; +part 'model/session_update_dto.dart'; part 'model/shared_link_create_dto.dart'; part 'model/shared_link_edit_dto.dart'; part 'model/shared_link_response_dto.dart'; @@ -249,18 +250,33 @@ part 'model/sync_ack_delete_dto.dart'; part 'model/sync_ack_dto.dart'; part 'model/sync_ack_set_dto.dart'; part 'model/sync_album_delete_v1.dart'; +part 'model/sync_album_to_asset_delete_v1.dart'; +part 'model/sync_album_to_asset_v1.dart'; part 'model/sync_album_user_delete_v1.dart'; part 'model/sync_album_user_v1.dart'; part 'model/sync_album_v1.dart'; part 'model/sync_asset_delete_v1.dart'; part 'model/sync_asset_exif_v1.dart'; +part 'model/sync_asset_face_delete_v1.dart'; +part 'model/sync_asset_face_v1.dart'; part 'model/sync_asset_v1.dart'; +part 'model/sync_auth_user_v1.dart'; part 'model/sync_entity_type.dart'; +part 'model/sync_memory_asset_delete_v1.dart'; +part 'model/sync_memory_asset_v1.dart'; +part 'model/sync_memory_delete_v1.dart'; +part 'model/sync_memory_v1.dart'; part 'model/sync_partner_delete_v1.dart'; part 'model/sync_partner_v1.dart'; +part 'model/sync_person_delete_v1.dart'; +part 'model/sync_person_v1.dart'; part 'model/sync_request_type.dart'; +part 'model/sync_stack_delete_v1.dart'; +part 'model/sync_stack_v1.dart'; part 'model/sync_stream_dto.dart'; part 'model/sync_user_delete_v1.dart'; +part 'model/sync_user_metadata_delete_v1.dart'; +part 'model/sync_user_metadata_v1.dart'; part 'model/sync_user_v1.dart'; part 'model/system_config_backups_dto.dart'; part 'model/system_config_dto.dart'; @@ -278,6 +294,7 @@ part 'model/system_config_machine_learning_dto.dart'; part 'model/system_config_map_dto.dart'; part 'model/system_config_metadata_dto.dart'; part 'model/system_config_new_version_check_dto.dart'; +part 'model/system_config_nightly_tasks_dto.dart'; part 'model/system_config_notifications_dto.dart'; part 'model/system_config_o_auth_dto.dart'; part 'model/system_config_password_login_dto.dart'; @@ -321,6 +338,7 @@ part 'model/user_admin_response_dto.dart'; part 'model/user_admin_update_dto.dart'; part 'model/user_avatar_color.dart'; part 'model/user_license.dart'; +part 'model/user_metadata_key.dart'; part 'model/user_preferences_response_dto.dart'; part 'model/user_preferences_update_dto.dart'; part 'model/user_response_dto.dart'; diff --git a/mobile/openapi/lib/api/people_api.dart b/mobile/openapi/lib/api/people_api.dart index 1cdb878852..35dbac4e97 100644 --- a/mobile/openapi/lib/api/people_api.dart +++ b/mobile/openapi/lib/api/people_api.dart @@ -63,6 +63,85 @@ class PeopleApi { return null; } + /// Performs an HTTP 'DELETE /people' operation and returns the [Response]. + /// Parameters: + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future deletePeopleWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/people'; + + // ignore: prefer_final_locals + Object? postBody = bulkIdsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future deletePeople(BulkIdsDto bulkIdsDto,) async { + final response = await deletePeopleWithHttpInfo(bulkIdsDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'DELETE /people/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future deletePersonWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final apiPath = r'/people/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future deletePerson(String id,) async { + final response = await deletePersonWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'GET /people' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api/sessions_api.dart b/mobile/openapi/lib/api/sessions_api.dart index 3228d31e91..d54f520641 100644 --- a/mobile/openapi/lib/api/sessions_api.dart +++ b/mobile/openapi/lib/api/sessions_api.dart @@ -219,4 +219,56 @@ class SessionsApi { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } } + + /// Performs an HTTP 'PUT /sessions/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [SessionUpdateDto] sessionUpdateDto (required): + Future updateSessionWithHttpInfo(String id, SessionUpdateDto sessionUpdateDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/sessions/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = sessionUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [SessionUpdateDto] sessionUpdateDto (required): + Future updateSession(String id, SessionUpdateDto sessionUpdateDto,) async { + final response = await updateSessionWithHttpInfo(id, sessionUpdateDto,); + 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), 'SessionResponseDto',) as SessionResponseDto; + + } + return null; + } } diff --git a/mobile/openapi/lib/api/stacks_api.dart b/mobile/openapi/lib/api/stacks_api.dart index 84f23ec55d..6d6c4506be 100644 --- a/mobile/openapi/lib/api/stacks_api.dart +++ b/mobile/openapi/lib/api/stacks_api.dart @@ -190,6 +190,51 @@ class StacksApi { return null; } + /// Performs an HTTP 'DELETE /stacks/{id}/assets/{assetId}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] assetId (required): + /// + /// * [String] id (required): + Future removeAssetFromStackWithHttpInfo(String assetId, String id,) async { + // ignore: prefer_const_declarations + final apiPath = r'/stacks/{id}/assets/{assetId}' + .replaceAll('{assetId}', assetId) + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] assetId (required): + /// + /// * [String] id (required): + Future removeAssetFromStack(String assetId, String id,) async { + final response = await removeAssetFromStackWithHttpInfo(assetId, id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'GET /stacks' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 28b67f52c5..bd306cb216 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -520,6 +520,8 @@ class ApiClient { return SessionResponseDto.fromJson(value); case 'SessionUnlockDto': return SessionUnlockDto.fromJson(value); + case 'SessionUpdateDto': + return SessionUpdateDto.fromJson(value); case 'SharedLinkCreateDto': return SharedLinkCreateDto.fromJson(value); case 'SharedLinkEditDto': @@ -554,6 +556,10 @@ class ApiClient { return SyncAckSetDto.fromJson(value); case 'SyncAlbumDeleteV1': return SyncAlbumDeleteV1.fromJson(value); + case 'SyncAlbumToAssetDeleteV1': + return SyncAlbumToAssetDeleteV1.fromJson(value); + case 'SyncAlbumToAssetV1': + return SyncAlbumToAssetV1.fromJson(value); case 'SyncAlbumUserDeleteV1': return SyncAlbumUserDeleteV1.fromJson(value); case 'SyncAlbumUserV1': @@ -564,20 +570,46 @@ class ApiClient { return SyncAssetDeleteV1.fromJson(value); case 'SyncAssetExifV1': return SyncAssetExifV1.fromJson(value); + case 'SyncAssetFaceDeleteV1': + return SyncAssetFaceDeleteV1.fromJson(value); + case 'SyncAssetFaceV1': + return SyncAssetFaceV1.fromJson(value); case 'SyncAssetV1': return SyncAssetV1.fromJson(value); + case 'SyncAuthUserV1': + return SyncAuthUserV1.fromJson(value); case 'SyncEntityType': return SyncEntityTypeTypeTransformer().decode(value); + case 'SyncMemoryAssetDeleteV1': + return SyncMemoryAssetDeleteV1.fromJson(value); + case 'SyncMemoryAssetV1': + return SyncMemoryAssetV1.fromJson(value); + case 'SyncMemoryDeleteV1': + return SyncMemoryDeleteV1.fromJson(value); + case 'SyncMemoryV1': + return SyncMemoryV1.fromJson(value); case 'SyncPartnerDeleteV1': return SyncPartnerDeleteV1.fromJson(value); case 'SyncPartnerV1': return SyncPartnerV1.fromJson(value); + case 'SyncPersonDeleteV1': + return SyncPersonDeleteV1.fromJson(value); + case 'SyncPersonV1': + return SyncPersonV1.fromJson(value); case 'SyncRequestType': return SyncRequestTypeTypeTransformer().decode(value); + case 'SyncStackDeleteV1': + return SyncStackDeleteV1.fromJson(value); + case 'SyncStackV1': + return SyncStackV1.fromJson(value); case 'SyncStreamDto': return SyncStreamDto.fromJson(value); case 'SyncUserDeleteV1': return SyncUserDeleteV1.fromJson(value); + case 'SyncUserMetadataDeleteV1': + return SyncUserMetadataDeleteV1.fromJson(value); + case 'SyncUserMetadataV1': + return SyncUserMetadataV1.fromJson(value); case 'SyncUserV1': return SyncUserV1.fromJson(value); case 'SystemConfigBackupsDto': @@ -612,6 +644,8 @@ class ApiClient { return SystemConfigMetadataDto.fromJson(value); case 'SystemConfigNewVersionCheckDto': return SystemConfigNewVersionCheckDto.fromJson(value); + case 'SystemConfigNightlyTasksDto': + return SystemConfigNightlyTasksDto.fromJson(value); case 'SystemConfigNotificationsDto': return SystemConfigNotificationsDto.fromJson(value); case 'SystemConfigOAuthDto': @@ -698,6 +732,8 @@ class ApiClient { return UserAvatarColorTypeTransformer().decode(value); case 'UserLicense': return UserLicense.fromJson(value); + case 'UserMetadataKey': + return UserMetadataKeyTypeTransformer().decode(value); case 'UserPreferencesResponseDto': return UserPreferencesResponseDto.fromJson(value); case 'UserPreferencesUpdateDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 1618f4a670..098d32f4f4 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -151,6 +151,9 @@ String parameterToString(dynamic value) { if (value is UserAvatarColor) { return UserAvatarColorTypeTransformer().encode(value).toString(); } + if (value is UserMetadataKey) { + return UserMetadataKeyTypeTransformer().encode(value).toString(); + } if (value is UserStatus) { return UserStatusTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 520777a45d..b7e637d4b4 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -241,7 +241,7 @@ class MetadataSearchDto { String? state; - List tagIds; + List? tagIds; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -425,7 +425,7 @@ class MetadataSearchDto { (rating == null ? 0 : rating!.hashCode) + (size == null ? 0 : size!.hashCode) + (state == null ? 0 : state!.hashCode) + - (tagIds.hashCode) + + (tagIds == null ? 0 : tagIds!.hashCode) + (takenAfter == null ? 0 : takenAfter!.hashCode) + (takenBefore == null ? 0 : takenBefore!.hashCode) + (thumbnailPath == null ? 0 : thumbnailPath!.hashCode) + @@ -578,7 +578,11 @@ class MetadataSearchDto { } else { // json[r'state'] = null; } + if (this.tagIds != null) { json[r'tagIds'] = this.tagIds; + } else { + // json[r'tagIds'] = null; + } if (this.takenAfter != null) { json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); } else { diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index a85b5002bf..ec67d81be4 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -36,25 +36,35 @@ class Permission { static const assetPeriodRead = Permission._(r'asset.read'); static const assetPeriodUpdate = Permission._(r'asset.update'); static const assetPeriodDelete = Permission._(r'asset.delete'); + static const assetPeriodStatistics = Permission._(r'asset.statistics'); static const assetPeriodShare = Permission._(r'asset.share'); 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 albumPeriodCreate = Permission._(r'album.create'); static const albumPeriodRead = Permission._(r'album.read'); static const albumPeriodUpdate = Permission._(r'album.update'); static const albumPeriodDelete = Permission._(r'album.delete'); static const albumPeriodStatistics = Permission._(r'album.statistics'); - static const albumPeriodAddAsset = Permission._(r'album.addAsset'); - static const albumPeriodRemoveAsset = Permission._(r'album.removeAsset'); static const albumPeriodShare = Permission._(r'album.share'); static const albumPeriodDownload = Permission._(r'album.download'); + static const albumAssetPeriodCreate = Permission._(r'albumAsset.create'); + static const albumAssetPeriodDelete = Permission._(r'albumAsset.delete'); + static const albumUserPeriodCreate = Permission._(r'albumUser.create'); + static const albumUserPeriodUpdate = Permission._(r'albumUser.update'); + static const albumUserPeriodDelete = Permission._(r'albumUser.delete'); + static const authPeriodChangePassword = Permission._(r'auth.changePassword'); static const authDevicePeriodDelete = Permission._(r'authDevice.delete'); static const archivePeriodRead = Permission._(r'archive.read'); + static const duplicatePeriodRead = Permission._(r'duplicate.read'); + static const duplicatePeriodDelete = Permission._(r'duplicate.delete'); static const facePeriodCreate = Permission._(r'face.create'); static const facePeriodRead = Permission._(r'face.read'); static const facePeriodUpdate = Permission._(r'face.update'); static const facePeriodDelete = Permission._(r'face.delete'); + static const jobPeriodCreate = Permission._(r'job.create'); + static const jobPeriodRead = Permission._(r'job.read'); static const libraryPeriodCreate = Permission._(r'library.create'); static const libraryPeriodRead = Permission._(r'library.read'); static const libraryPeriodUpdate = Permission._(r'library.update'); @@ -66,6 +76,9 @@ class Permission { static const memoryPeriodRead = Permission._(r'memory.read'); static const memoryPeriodUpdate = Permission._(r'memory.update'); static const memoryPeriodDelete = Permission._(r'memory.delete'); + static const memoryPeriodStatistics = Permission._(r'memory.statistics'); + static const memoryAssetPeriodCreate = Permission._(r'memoryAsset.create'); + static const memoryAssetPeriodDelete = Permission._(r'memoryAsset.delete'); static const notificationPeriodCreate = Permission._(r'notification.create'); static const notificationPeriodRead = Permission._(r'notification.read'); static const notificationPeriodUpdate = Permission._(r'notification.update'); @@ -81,6 +94,16 @@ class Permission { static const personPeriodStatistics = Permission._(r'person.statistics'); static const personPeriodMerge = Permission._(r'person.merge'); static const personPeriodReassign = Permission._(r'person.reassign'); + static const pinCodePeriodCreate = Permission._(r'pinCode.create'); + static const pinCodePeriodUpdate = Permission._(r'pinCode.update'); + static const pinCodePeriodDelete = Permission._(r'pinCode.delete'); + static const serverPeriodAbout = Permission._(r'server.about'); + static const serverPeriodApkLinks = Permission._(r'server.apkLinks'); + static const serverPeriodStorage = Permission._(r'server.storage'); + static const serverPeriodStatistics = Permission._(r'server.statistics'); + static const serverLicensePeriodRead = Permission._(r'serverLicense.read'); + static const serverLicensePeriodUpdate = Permission._(r'serverLicense.update'); + static const serverLicensePeriodDelete = Permission._(r'serverLicense.delete'); static const sessionPeriodCreate = Permission._(r'session.create'); static const sessionPeriodRead = Permission._(r'session.read'); static const sessionPeriodUpdate = Permission._(r'session.update'); @@ -94,6 +117,10 @@ class Permission { static const stackPeriodRead = Permission._(r'stack.read'); static const stackPeriodUpdate = Permission._(r'stack.update'); static const stackPeriodDelete = Permission._(r'stack.delete'); + static const syncPeriodStream = Permission._(r'sync.stream'); + static const syncCheckpointPeriodRead = Permission._(r'syncCheckpoint.read'); + static const syncCheckpointPeriodUpdate = Permission._(r'syncCheckpoint.update'); + static const syncCheckpointPeriodDelete = Permission._(r'syncCheckpoint.delete'); static const systemConfigPeriodRead = Permission._(r'systemConfig.read'); static const systemConfigPeriodUpdate = Permission._(r'systemConfig.update'); static const systemMetadataPeriodRead = Permission._(r'systemMetadata.read'); @@ -103,10 +130,25 @@ class Permission { static const tagPeriodUpdate = Permission._(r'tag.update'); static const tagPeriodDelete = Permission._(r'tag.delete'); static const tagPeriodAsset = Permission._(r'tag.asset'); - static const adminPeriodUserPeriodCreate = Permission._(r'admin.user.create'); - static const adminPeriodUserPeriodRead = Permission._(r'admin.user.read'); - static const adminPeriodUserPeriodUpdate = Permission._(r'admin.user.update'); - static const adminPeriodUserPeriodDelete = Permission._(r'admin.user.delete'); + static const userPeriodRead = Permission._(r'user.read'); + static const userPeriodUpdate = Permission._(r'user.update'); + static const userLicensePeriodCreate = Permission._(r'userLicense.create'); + static const userLicensePeriodRead = Permission._(r'userLicense.read'); + static const userLicensePeriodUpdate = Permission._(r'userLicense.update'); + static const userLicensePeriodDelete = Permission._(r'userLicense.delete'); + static const userOnboardingPeriodRead = Permission._(r'userOnboarding.read'); + static const userOnboardingPeriodUpdate = Permission._(r'userOnboarding.update'); + static const userOnboardingPeriodDelete = Permission._(r'userOnboarding.delete'); + static const userPreferencePeriodRead = Permission._(r'userPreference.read'); + static const userPreferencePeriodUpdate = Permission._(r'userPreference.update'); + static const userProfileImagePeriodCreate = Permission._(r'userProfileImage.create'); + static const userProfileImagePeriodRead = Permission._(r'userProfileImage.read'); + static const userProfileImagePeriodUpdate = Permission._(r'userProfileImage.update'); + static const userProfileImagePeriodDelete = Permission._(r'userProfileImage.delete'); + static const adminUserPeriodCreate = Permission._(r'adminUser.create'); + static const adminUserPeriodRead = Permission._(r'adminUser.read'); + static const adminUserPeriodUpdate = Permission._(r'adminUser.update'); + static const adminUserPeriodDelete = Permission._(r'adminUser.delete'); /// List of all possible values in this [enum][Permission]. static const values = [ @@ -123,25 +165,35 @@ class Permission { assetPeriodRead, assetPeriodUpdate, assetPeriodDelete, + assetPeriodStatistics, assetPeriodShare, assetPeriodView, assetPeriodDownload, assetPeriodUpload, + assetPeriodReplace, albumPeriodCreate, albumPeriodRead, albumPeriodUpdate, albumPeriodDelete, albumPeriodStatistics, - albumPeriodAddAsset, - albumPeriodRemoveAsset, albumPeriodShare, albumPeriodDownload, + albumAssetPeriodCreate, + albumAssetPeriodDelete, + albumUserPeriodCreate, + albumUserPeriodUpdate, + albumUserPeriodDelete, + authPeriodChangePassword, authDevicePeriodDelete, archivePeriodRead, + duplicatePeriodRead, + duplicatePeriodDelete, facePeriodCreate, facePeriodRead, facePeriodUpdate, facePeriodDelete, + jobPeriodCreate, + jobPeriodRead, libraryPeriodCreate, libraryPeriodRead, libraryPeriodUpdate, @@ -153,6 +205,9 @@ class Permission { memoryPeriodRead, memoryPeriodUpdate, memoryPeriodDelete, + memoryPeriodStatistics, + memoryAssetPeriodCreate, + memoryAssetPeriodDelete, notificationPeriodCreate, notificationPeriodRead, notificationPeriodUpdate, @@ -168,6 +223,16 @@ class Permission { personPeriodStatistics, personPeriodMerge, personPeriodReassign, + pinCodePeriodCreate, + pinCodePeriodUpdate, + pinCodePeriodDelete, + serverPeriodAbout, + serverPeriodApkLinks, + serverPeriodStorage, + serverPeriodStatistics, + serverLicensePeriodRead, + serverLicensePeriodUpdate, + serverLicensePeriodDelete, sessionPeriodCreate, sessionPeriodRead, sessionPeriodUpdate, @@ -181,6 +246,10 @@ class Permission { stackPeriodRead, stackPeriodUpdate, stackPeriodDelete, + syncPeriodStream, + syncCheckpointPeriodRead, + syncCheckpointPeriodUpdate, + syncCheckpointPeriodDelete, systemConfigPeriodRead, systemConfigPeriodUpdate, systemMetadataPeriodRead, @@ -190,10 +259,25 @@ class Permission { tagPeriodUpdate, tagPeriodDelete, tagPeriodAsset, - adminPeriodUserPeriodCreate, - adminPeriodUserPeriodRead, - adminPeriodUserPeriodUpdate, - adminPeriodUserPeriodDelete, + userPeriodRead, + userPeriodUpdate, + userLicensePeriodCreate, + userLicensePeriodRead, + userLicensePeriodUpdate, + userLicensePeriodDelete, + userOnboardingPeriodRead, + userOnboardingPeriodUpdate, + userOnboardingPeriodDelete, + userPreferencePeriodRead, + userPreferencePeriodUpdate, + userProfileImagePeriodCreate, + userProfileImagePeriodRead, + userProfileImagePeriodUpdate, + userProfileImagePeriodDelete, + adminUserPeriodCreate, + adminUserPeriodRead, + adminUserPeriodUpdate, + adminUserPeriodDelete, ]; static Permission? fromJson(dynamic value) => PermissionTypeTransformer().decode(value); @@ -245,25 +329,35 @@ class PermissionTypeTransformer { case r'asset.read': return Permission.assetPeriodRead; case r'asset.update': return Permission.assetPeriodUpdate; case r'asset.delete': return Permission.assetPeriodDelete; + case r'asset.statistics': return Permission.assetPeriodStatistics; case r'asset.share': return Permission.assetPeriodShare; 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'album.create': return Permission.albumPeriodCreate; case r'album.read': return Permission.albumPeriodRead; case r'album.update': return Permission.albumPeriodUpdate; case r'album.delete': return Permission.albumPeriodDelete; case r'album.statistics': return Permission.albumPeriodStatistics; - case r'album.addAsset': return Permission.albumPeriodAddAsset; - case r'album.removeAsset': return Permission.albumPeriodRemoveAsset; case r'album.share': return Permission.albumPeriodShare; case r'album.download': return Permission.albumPeriodDownload; + case r'albumAsset.create': return Permission.albumAssetPeriodCreate; + case r'albumAsset.delete': return Permission.albumAssetPeriodDelete; + case r'albumUser.create': return Permission.albumUserPeriodCreate; + case r'albumUser.update': return Permission.albumUserPeriodUpdate; + case r'albumUser.delete': return Permission.albumUserPeriodDelete; + case r'auth.changePassword': return Permission.authPeriodChangePassword; case r'authDevice.delete': return Permission.authDevicePeriodDelete; case r'archive.read': return Permission.archivePeriodRead; + case r'duplicate.read': return Permission.duplicatePeriodRead; + case r'duplicate.delete': return Permission.duplicatePeriodDelete; case r'face.create': return Permission.facePeriodCreate; case r'face.read': return Permission.facePeriodRead; case r'face.update': return Permission.facePeriodUpdate; case r'face.delete': return Permission.facePeriodDelete; + case r'job.create': return Permission.jobPeriodCreate; + case r'job.read': return Permission.jobPeriodRead; case r'library.create': return Permission.libraryPeriodCreate; case r'library.read': return Permission.libraryPeriodRead; case r'library.update': return Permission.libraryPeriodUpdate; @@ -275,6 +369,9 @@ class PermissionTypeTransformer { case r'memory.read': return Permission.memoryPeriodRead; case r'memory.update': return Permission.memoryPeriodUpdate; case r'memory.delete': return Permission.memoryPeriodDelete; + case r'memory.statistics': return Permission.memoryPeriodStatistics; + case r'memoryAsset.create': return Permission.memoryAssetPeriodCreate; + case r'memoryAsset.delete': return Permission.memoryAssetPeriodDelete; case r'notification.create': return Permission.notificationPeriodCreate; case r'notification.read': return Permission.notificationPeriodRead; case r'notification.update': return Permission.notificationPeriodUpdate; @@ -290,6 +387,16 @@ class PermissionTypeTransformer { case r'person.statistics': return Permission.personPeriodStatistics; case r'person.merge': return Permission.personPeriodMerge; case r'person.reassign': return Permission.personPeriodReassign; + case r'pinCode.create': return Permission.pinCodePeriodCreate; + case r'pinCode.update': return Permission.pinCodePeriodUpdate; + case r'pinCode.delete': return Permission.pinCodePeriodDelete; + case r'server.about': return Permission.serverPeriodAbout; + case r'server.apkLinks': return Permission.serverPeriodApkLinks; + case r'server.storage': return Permission.serverPeriodStorage; + case r'server.statistics': return Permission.serverPeriodStatistics; + case r'serverLicense.read': return Permission.serverLicensePeriodRead; + case r'serverLicense.update': return Permission.serverLicensePeriodUpdate; + case r'serverLicense.delete': return Permission.serverLicensePeriodDelete; case r'session.create': return Permission.sessionPeriodCreate; case r'session.read': return Permission.sessionPeriodRead; case r'session.update': return Permission.sessionPeriodUpdate; @@ -303,6 +410,10 @@ class PermissionTypeTransformer { case r'stack.read': return Permission.stackPeriodRead; case r'stack.update': return Permission.stackPeriodUpdate; case r'stack.delete': return Permission.stackPeriodDelete; + case r'sync.stream': return Permission.syncPeriodStream; + case r'syncCheckpoint.read': return Permission.syncCheckpointPeriodRead; + case r'syncCheckpoint.update': return Permission.syncCheckpointPeriodUpdate; + case r'syncCheckpoint.delete': return Permission.syncCheckpointPeriodDelete; case r'systemConfig.read': return Permission.systemConfigPeriodRead; case r'systemConfig.update': return Permission.systemConfigPeriodUpdate; case r'systemMetadata.read': return Permission.systemMetadataPeriodRead; @@ -312,10 +423,25 @@ class PermissionTypeTransformer { case r'tag.update': return Permission.tagPeriodUpdate; case r'tag.delete': return Permission.tagPeriodDelete; case r'tag.asset': return Permission.tagPeriodAsset; - case r'admin.user.create': return Permission.adminPeriodUserPeriodCreate; - case r'admin.user.read': return Permission.adminPeriodUserPeriodRead; - case r'admin.user.update': return Permission.adminPeriodUserPeriodUpdate; - case r'admin.user.delete': return Permission.adminPeriodUserPeriodDelete; + case r'user.read': return Permission.userPeriodRead; + case r'user.update': return Permission.userPeriodUpdate; + case r'userLicense.create': return Permission.userLicensePeriodCreate; + case r'userLicense.read': return Permission.userLicensePeriodRead; + case r'userLicense.update': return Permission.userLicensePeriodUpdate; + case r'userLicense.delete': return Permission.userLicensePeriodDelete; + case r'userOnboarding.read': return Permission.userOnboardingPeriodRead; + case r'userOnboarding.update': return Permission.userOnboardingPeriodUpdate; + case r'userOnboarding.delete': return Permission.userOnboardingPeriodDelete; + case r'userPreference.read': return Permission.userPreferencePeriodRead; + case r'userPreference.update': return Permission.userPreferencePeriodUpdate; + case r'userProfileImage.create': return Permission.userProfileImagePeriodCreate; + case r'userProfileImage.read': return Permission.userProfileImagePeriodRead; + case r'userProfileImage.update': return Permission.userProfileImagePeriodUpdate; + case r'userProfileImage.delete': return Permission.userProfileImagePeriodDelete; + case r'adminUser.create': return Permission.adminUserPeriodCreate; + case r'adminUser.read': return Permission.adminUserPeriodRead; + case r'adminUser.update': return Permission.adminUserPeriodUpdate; + case r'adminUser.delete': return Permission.adminUserPeriodDelete; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart index c5914f9fa3..98cc715af4 100644 --- a/mobile/openapi/lib/model/random_search_dto.dart +++ b/mobile/openapi/lib/model/random_search_dto.dart @@ -155,7 +155,7 @@ class RandomSearchDto { String? state; - List tagIds; + List? tagIds; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -310,7 +310,7 @@ class RandomSearchDto { (rating == null ? 0 : rating!.hashCode) + (size == null ? 0 : size!.hashCode) + (state == null ? 0 : state!.hashCode) + - (tagIds.hashCode) + + (tagIds == null ? 0 : tagIds!.hashCode) + (takenAfter == null ? 0 : takenAfter!.hashCode) + (takenBefore == null ? 0 : takenBefore!.hashCode) + (trashedAfter == null ? 0 : trashedAfter!.hashCode) + @@ -416,7 +416,11 @@ class RandomSearchDto { } else { // json[r'state'] = null; } + if (this.tagIds != null) { json[r'tagIds'] = this.tagIds; + } else { + // json[r'tagIds'] = null; + } if (this.takenAfter != null) { json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); } else { diff --git a/mobile/openapi/lib/model/session_create_response_dto.dart b/mobile/openapi/lib/model/session_create_response_dto.dart index ab1c4ca2d8..a4f93e8d9c 100644 --- a/mobile/openapi/lib/model/session_create_response_dto.dart +++ b/mobile/openapi/lib/model/session_create_response_dto.dart @@ -19,6 +19,7 @@ class SessionCreateResponseDto { required this.deviceType, this.expiresAt, required this.id, + required this.isPendingSyncReset, required this.token, required this.updatedAt, }); @@ -41,6 +42,8 @@ class SessionCreateResponseDto { String id; + bool isPendingSyncReset; + String token; String updatedAt; @@ -53,6 +56,7 @@ class SessionCreateResponseDto { other.deviceType == deviceType && other.expiresAt == expiresAt && other.id == id && + other.isPendingSyncReset == isPendingSyncReset && other.token == token && other.updatedAt == updatedAt; @@ -65,11 +69,12 @@ class SessionCreateResponseDto { (deviceType.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) + (id.hashCode) + + (isPendingSyncReset.hashCode) + (token.hashCode) + (updatedAt.hashCode); @override - String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, token=$token, updatedAt=$updatedAt]'; + String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, token=$token, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -83,6 +88,7 @@ class SessionCreateResponseDto { // json[r'expiresAt'] = null; } json[r'id'] = this.id; + json[r'isPendingSyncReset'] = this.isPendingSyncReset; json[r'token'] = this.token; json[r'updatedAt'] = this.updatedAt; return json; @@ -103,6 +109,7 @@ class SessionCreateResponseDto { deviceType: mapValueOfType(json, r'deviceType')!, expiresAt: mapValueOfType(json, r'expiresAt'), id: mapValueOfType(json, r'id')!, + isPendingSyncReset: mapValueOfType(json, r'isPendingSyncReset')!, token: mapValueOfType(json, r'token')!, updatedAt: mapValueOfType(json, r'updatedAt')!, ); @@ -157,6 +164,7 @@ class SessionCreateResponseDto { 'deviceOS', 'deviceType', 'id', + 'isPendingSyncReset', 'token', 'updatedAt', }; diff --git a/mobile/openapi/lib/model/session_response_dto.dart b/mobile/openapi/lib/model/session_response_dto.dart index cf9eb08a78..e76e4d48b4 100644 --- a/mobile/openapi/lib/model/session_response_dto.dart +++ b/mobile/openapi/lib/model/session_response_dto.dart @@ -19,6 +19,7 @@ class SessionResponseDto { required this.deviceType, this.expiresAt, required this.id, + required this.isPendingSyncReset, required this.updatedAt, }); @@ -40,6 +41,8 @@ class SessionResponseDto { String id; + bool isPendingSyncReset; + String updatedAt; @override @@ -50,6 +53,7 @@ class SessionResponseDto { other.deviceType == deviceType && other.expiresAt == expiresAt && other.id == id && + other.isPendingSyncReset == isPendingSyncReset && other.updatedAt == updatedAt; @override @@ -61,10 +65,11 @@ class SessionResponseDto { (deviceType.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) + (id.hashCode) + + (isPendingSyncReset.hashCode) + (updatedAt.hashCode); @override - String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, updatedAt=$updatedAt]'; + String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -78,6 +83,7 @@ class SessionResponseDto { // json[r'expiresAt'] = null; } json[r'id'] = this.id; + json[r'isPendingSyncReset'] = this.isPendingSyncReset; json[r'updatedAt'] = this.updatedAt; return json; } @@ -97,6 +103,7 @@ class SessionResponseDto { deviceType: mapValueOfType(json, r'deviceType')!, expiresAt: mapValueOfType(json, r'expiresAt'), id: mapValueOfType(json, r'id')!, + isPendingSyncReset: mapValueOfType(json, r'isPendingSyncReset')!, updatedAt: mapValueOfType(json, r'updatedAt')!, ); } @@ -150,6 +157,7 @@ class SessionResponseDto { 'deviceOS', 'deviceType', 'id', + 'isPendingSyncReset', 'updatedAt', }; } diff --git a/mobile/openapi/lib/model/session_update_dto.dart b/mobile/openapi/lib/model/session_update_dto.dart new file mode 100644 index 0000000000..cd170b1baa --- /dev/null +++ b/mobile/openapi/lib/model/session_update_dto.dart @@ -0,0 +1,108 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SessionUpdateDto { + /// Returns a new [SessionUpdateDto] instance. + SessionUpdateDto({ + this.isPendingSyncReset, + }); + + /// + /// 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? isPendingSyncReset; + + @override + bool operator ==(Object other) => identical(this, other) || other is SessionUpdateDto && + other.isPendingSyncReset == isPendingSyncReset; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isPendingSyncReset == null ? 0 : isPendingSyncReset!.hashCode); + + @override + String toString() => 'SessionUpdateDto[isPendingSyncReset=$isPendingSyncReset]'; + + Map toJson() { + final json = {}; + if (this.isPendingSyncReset != null) { + json[r'isPendingSyncReset'] = this.isPendingSyncReset; + } else { + // json[r'isPendingSyncReset'] = null; + } + return json; + } + + /// Returns a new [SessionUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SessionUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "SessionUpdateDto"); + if (value is Map) { + final json = value.cast(); + + return SessionUpdateDto( + isPendingSyncReset: mapValueOfType(json, r'isPendingSyncReset'), + ); + } + 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 = SessionUpdateDto.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 = SessionUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SessionUpdateDto-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] = SessionUpdateDto.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/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index c221340553..0d16b56d74 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -175,7 +175,7 @@ class SmartSearchDto { String? state; - List tagIds; + List? tagIds; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -318,7 +318,7 @@ class SmartSearchDto { (rating == null ? 0 : rating!.hashCode) + (size == null ? 0 : size!.hashCode) + (state == null ? 0 : state!.hashCode) + - (tagIds.hashCode) + + (tagIds == null ? 0 : tagIds!.hashCode) + (takenAfter == null ? 0 : takenAfter!.hashCode) + (takenBefore == null ? 0 : takenBefore!.hashCode) + (trashedAfter == null ? 0 : trashedAfter!.hashCode) + @@ -433,7 +433,11 @@ class SmartSearchDto { } else { // json[r'state'] = null; } + if (this.tagIds != null) { json[r'tagIds'] = this.tagIds; + } else { + // json[r'tagIds'] = null; + } if (this.takenAfter != null) { json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); } else { diff --git a/mobile/openapi/lib/model/statistics_search_dto.dart b/mobile/openapi/lib/model/statistics_search_dto.dart index 55de23ba32..73d80c9e36 100644 --- a/mobile/openapi/lib/model/statistics_search_dto.dart +++ b/mobile/openapi/lib/model/statistics_search_dto.dart @@ -149,7 +149,7 @@ class StatisticsSearchDto { String? state; - List tagIds; + List? tagIds; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -268,7 +268,7 @@ class StatisticsSearchDto { (personIds.hashCode) + (rating == null ? 0 : rating!.hashCode) + (state == null ? 0 : state!.hashCode) + - (tagIds.hashCode) + + (tagIds == null ? 0 : tagIds!.hashCode) + (takenAfter == null ? 0 : takenAfter!.hashCode) + (takenBefore == null ? 0 : takenBefore!.hashCode) + (trashedAfter == null ? 0 : trashedAfter!.hashCode) + @@ -370,7 +370,11 @@ class StatisticsSearchDto { } else { // json[r'state'] = null; } + if (this.tagIds != null) { json[r'tagIds'] = this.tagIds; + } else { + // json[r'tagIds'] = null; + } if (this.takenAfter != null) { json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); } else { diff --git a/mobile/openapi/lib/model/sync_album_to_asset_delete_v1.dart b/mobile/openapi/lib/model/sync_album_to_asset_delete_v1.dart new file mode 100644 index 0000000000..d18c850b2a --- /dev/null +++ b/mobile/openapi/lib/model/sync_album_to_asset_delete_v1.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncAlbumToAssetDeleteV1 { + /// Returns a new [SyncAlbumToAssetDeleteV1] instance. + SyncAlbumToAssetDeleteV1({ + required this.albumId, + required this.assetId, + }); + + String albumId; + + String assetId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAlbumToAssetDeleteV1 && + other.albumId == albumId && + other.assetId == assetId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (albumId.hashCode) + + (assetId.hashCode); + + @override + String toString() => 'SyncAlbumToAssetDeleteV1[albumId=$albumId, assetId=$assetId]'; + + Map toJson() { + final json = {}; + json[r'albumId'] = this.albumId; + json[r'assetId'] = this.assetId; + return json; + } + + /// Returns a new [SyncAlbumToAssetDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAlbumToAssetDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAlbumToAssetDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAlbumToAssetDeleteV1( + albumId: mapValueOfType(json, r'albumId')!, + assetId: mapValueOfType(json, r'assetId')!, + ); + } + 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 = SyncAlbumToAssetDeleteV1.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 = SyncAlbumToAssetDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAlbumToAssetDeleteV1-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] = SyncAlbumToAssetDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'albumId', + 'assetId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_album_to_asset_v1.dart b/mobile/openapi/lib/model/sync_album_to_asset_v1.dart new file mode 100644 index 0000000000..6908f320f8 --- /dev/null +++ b/mobile/openapi/lib/model/sync_album_to_asset_v1.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncAlbumToAssetV1 { + /// Returns a new [SyncAlbumToAssetV1] instance. + SyncAlbumToAssetV1({ + required this.albumId, + required this.assetId, + }); + + String albumId; + + String assetId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAlbumToAssetV1 && + other.albumId == albumId && + other.assetId == assetId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (albumId.hashCode) + + (assetId.hashCode); + + @override + String toString() => 'SyncAlbumToAssetV1[albumId=$albumId, assetId=$assetId]'; + + Map toJson() { + final json = {}; + json[r'albumId'] = this.albumId; + json[r'assetId'] = this.assetId; + return json; + } + + /// Returns a new [SyncAlbumToAssetV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAlbumToAssetV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAlbumToAssetV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAlbumToAssetV1( + albumId: mapValueOfType(json, r'albumId')!, + assetId: mapValueOfType(json, r'assetId')!, + ); + } + 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 = SyncAlbumToAssetV1.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 = SyncAlbumToAssetV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAlbumToAssetV1-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] = SyncAlbumToAssetV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'albumId', + 'assetId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_asset_exif_v1.dart b/mobile/openapi/lib/model/sync_asset_exif_v1.dart index b0fef28b76..d4fdc9249d 100644 --- a/mobile/openapi/lib/model/sync_asset_exif_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_exif_v1.dart @@ -56,21 +56,21 @@ class SyncAssetExifV1 { String? exposureTime; - int? fNumber; + double? fNumber; int? fileSizeInByte; - int? focalLength; + double? focalLength; - int? fps; + double? fps; int? iso; - int? latitude; + double? latitude; String? lensModel; - int? longitude; + double? longitude; String? make; @@ -293,14 +293,14 @@ class SyncAssetExifV1 { exifImageHeight: mapValueOfType(json, r'exifImageHeight'), exifImageWidth: mapValueOfType(json, r'exifImageWidth'), exposureTime: mapValueOfType(json, r'exposureTime'), - fNumber: mapValueOfType(json, r'fNumber'), + fNumber: (mapValueOfType(json, r'fNumber'))?.toDouble(), fileSizeInByte: mapValueOfType(json, r'fileSizeInByte'), - focalLength: mapValueOfType(json, r'focalLength'), - fps: mapValueOfType(json, r'fps'), + focalLength: (mapValueOfType(json, r'focalLength'))?.toDouble(), + fps: (mapValueOfType(json, r'fps'))?.toDouble(), iso: mapValueOfType(json, r'iso'), - latitude: mapValueOfType(json, r'latitude'), + latitude: (mapValueOfType(json, r'latitude'))?.toDouble(), lensModel: mapValueOfType(json, r'lensModel'), - longitude: mapValueOfType(json, r'longitude'), + longitude: (mapValueOfType(json, r'longitude'))?.toDouble(), make: mapValueOfType(json, r'make'), model: mapValueOfType(json, r'model'), modifyDate: mapDateTime(json, r'modifyDate', r''), diff --git a/mobile/openapi/lib/model/sync_asset_face_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_face_delete_v1.dart new file mode 100644 index 0000000000..0992bfdcba --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_face_delete_v1.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncAssetFaceDeleteV1 { + /// Returns a new [SyncAssetFaceDeleteV1] instance. + SyncAssetFaceDeleteV1({ + required this.assetFaceId, + }); + + String assetFaceId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetFaceDeleteV1 && + other.assetFaceId == assetFaceId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetFaceId.hashCode); + + @override + String toString() => 'SyncAssetFaceDeleteV1[assetFaceId=$assetFaceId]'; + + Map toJson() { + final json = {}; + json[r'assetFaceId'] = this.assetFaceId; + return json; + } + + /// Returns a new [SyncAssetFaceDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetFaceDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetFaceDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetFaceDeleteV1( + assetFaceId: mapValueOfType(json, r'assetFaceId')!, + ); + } + 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 = SyncAssetFaceDeleteV1.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 = SyncAssetFaceDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetFaceDeleteV1-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] = SyncAssetFaceDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetFaceId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_asset_face_v1.dart b/mobile/openapi/lib/model/sync_asset_face_v1.dart new file mode 100644 index 0000000000..60d1766e34 --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_face_v1.dart @@ -0,0 +1,175 @@ +// +// 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 SyncAssetFaceV1 { + /// Returns a new [SyncAssetFaceV1] instance. + SyncAssetFaceV1({ + required this.assetId, + required this.boundingBoxX1, + required this.boundingBoxX2, + required this.boundingBoxY1, + required this.boundingBoxY2, + required this.id, + required this.imageHeight, + required this.imageWidth, + required this.personId, + required this.sourceType, + }); + + String assetId; + + int boundingBoxX1; + + int boundingBoxX2; + + int boundingBoxY1; + + int boundingBoxY2; + + String id; + + int imageHeight; + + int imageWidth; + + String? personId; + + String sourceType; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetFaceV1 && + other.assetId == assetId && + other.boundingBoxX1 == boundingBoxX1 && + other.boundingBoxX2 == boundingBoxX2 && + other.boundingBoxY1 == boundingBoxY1 && + other.boundingBoxY2 == boundingBoxY2 && + other.id == id && + other.imageHeight == imageHeight && + other.imageWidth == imageWidth && + other.personId == personId && + other.sourceType == sourceType; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (boundingBoxX1.hashCode) + + (boundingBoxX2.hashCode) + + (boundingBoxY1.hashCode) + + (boundingBoxY2.hashCode) + + (id.hashCode) + + (imageHeight.hashCode) + + (imageWidth.hashCode) + + (personId == null ? 0 : personId!.hashCode) + + (sourceType.hashCode); + + @override + String toString() => 'SyncAssetFaceV1[assetId=$assetId, boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, personId=$personId, sourceType=$sourceType]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'boundingBoxX1'] = this.boundingBoxX1; + json[r'boundingBoxX2'] = this.boundingBoxX2; + json[r'boundingBoxY1'] = this.boundingBoxY1; + json[r'boundingBoxY2'] = this.boundingBoxY2; + json[r'id'] = this.id; + json[r'imageHeight'] = this.imageHeight; + json[r'imageWidth'] = this.imageWidth; + if (this.personId != null) { + json[r'personId'] = this.personId; + } else { + // json[r'personId'] = null; + } + json[r'sourceType'] = this.sourceType; + return json; + } + + /// Returns a new [SyncAssetFaceV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetFaceV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetFaceV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetFaceV1( + assetId: mapValueOfType(json, r'assetId')!, + boundingBoxX1: mapValueOfType(json, r'boundingBoxX1')!, + boundingBoxX2: mapValueOfType(json, r'boundingBoxX2')!, + boundingBoxY1: mapValueOfType(json, r'boundingBoxY1')!, + boundingBoxY2: mapValueOfType(json, r'boundingBoxY2')!, + id: mapValueOfType(json, r'id')!, + imageHeight: mapValueOfType(json, r'imageHeight')!, + imageWidth: mapValueOfType(json, r'imageWidth')!, + personId: mapValueOfType(json, r'personId'), + sourceType: mapValueOfType(json, r'sourceType')!, + ); + } + 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 = SyncAssetFaceV1.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 = SyncAssetFaceV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetFaceV1-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] = SyncAssetFaceV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'boundingBoxX1', + 'boundingBoxX2', + 'boundingBoxY1', + 'boundingBoxY2', + 'id', + 'imageHeight', + 'imageWidth', + 'personId', + 'sourceType', + }; +} + diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart index 1ca6e20cff..4c42d08a5f 100644 --- a/mobile/openapi/lib/model/sync_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_v1.dart @@ -20,9 +20,11 @@ class SyncAssetV1 { required this.fileModifiedAt, required this.id, required this.isFavorite, + required this.livePhotoVideoId, required this.localDateTime, required this.originalFileName, required this.ownerId, + required this.stackId, required this.thumbhash, required this.type, required this.visibility, @@ -42,12 +44,16 @@ class SyncAssetV1 { bool isFavorite; + String? livePhotoVideoId; + DateTime? localDateTime; String originalFileName; String ownerId; + String? stackId; + String? thumbhash; AssetTypeEnum type; @@ -63,9 +69,11 @@ class SyncAssetV1 { other.fileModifiedAt == fileModifiedAt && other.id == id && other.isFavorite == isFavorite && + other.livePhotoVideoId == livePhotoVideoId && other.localDateTime == localDateTime && other.originalFileName == originalFileName && other.ownerId == ownerId && + other.stackId == stackId && other.thumbhash == thumbhash && other.type == type && other.visibility == visibility; @@ -80,15 +88,17 @@ class SyncAssetV1 { (fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) + (id.hashCode) + (isFavorite.hashCode) + + (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) + (localDateTime == null ? 0 : localDateTime!.hashCode) + (originalFileName.hashCode) + (ownerId.hashCode) + + (stackId == null ? 0 : stackId!.hashCode) + (thumbhash == null ? 0 : thumbhash!.hashCode) + (type.hashCode) + (visibility.hashCode); @override - String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, thumbhash=$thumbhash, type=$type, visibility=$visibility]'; + String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility]'; Map toJson() { final json = {}; @@ -115,6 +125,11 @@ class SyncAssetV1 { } json[r'id'] = this.id; json[r'isFavorite'] = this.isFavorite; + if (this.livePhotoVideoId != null) { + json[r'livePhotoVideoId'] = this.livePhotoVideoId; + } else { + // json[r'livePhotoVideoId'] = null; + } if (this.localDateTime != null) { json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String(); } else { @@ -122,6 +137,11 @@ class SyncAssetV1 { } json[r'originalFileName'] = this.originalFileName; json[r'ownerId'] = this.ownerId; + if (this.stackId != null) { + json[r'stackId'] = this.stackId; + } else { + // json[r'stackId'] = null; + } if (this.thumbhash != null) { json[r'thumbhash'] = this.thumbhash; } else { @@ -148,9 +168,11 @@ class SyncAssetV1 { fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''), id: mapValueOfType(json, r'id')!, isFavorite: mapValueOfType(json, r'isFavorite')!, + livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), localDateTime: mapDateTime(json, r'localDateTime', r''), originalFileName: mapValueOfType(json, r'originalFileName')!, ownerId: mapValueOfType(json, r'ownerId')!, + stackId: mapValueOfType(json, r'stackId'), thumbhash: mapValueOfType(json, r'thumbhash'), type: AssetTypeEnum.fromJson(json[r'type'])!, visibility: AssetVisibility.fromJson(json[r'visibility'])!, @@ -208,9 +230,11 @@ class SyncAssetV1 { 'fileModifiedAt', 'id', 'isFavorite', + 'livePhotoVideoId', 'localDateTime', 'originalFileName', 'ownerId', + 'stackId', 'thumbhash', 'type', 'visibility', diff --git a/mobile/openapi/lib/model/sync_auth_user_v1.dart b/mobile/openapi/lib/model/sync_auth_user_v1.dart new file mode 100644 index 0000000000..1dab7f47e3 --- /dev/null +++ b/mobile/openapi/lib/model/sync_auth_user_v1.dart @@ -0,0 +1,215 @@ +// +// 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 SyncAuthUserV1 { + /// Returns a new [SyncAuthUserV1] instance. + SyncAuthUserV1({ + required this.avatarColor, + required this.deletedAt, + required this.email, + required this.hasProfileImage, + required this.id, + required this.isAdmin, + required this.name, + required this.oauthId, + required this.pinCode, + required this.profileChangedAt, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + required this.storageLabel, + }); + + UserAvatarColor? avatarColor; + + DateTime? deletedAt; + + String email; + + bool hasProfileImage; + + String id; + + bool isAdmin; + + String name; + + String oauthId; + + String? pinCode; + + DateTime profileChangedAt; + + int? quotaSizeInBytes; + + int quotaUsageInBytes; + + String? storageLabel; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAuthUserV1 && + other.avatarColor == avatarColor && + other.deletedAt == deletedAt && + other.email == email && + other.hasProfileImage == hasProfileImage && + other.id == id && + other.isAdmin == isAdmin && + other.name == name && + other.oauthId == oauthId && + other.pinCode == pinCode && + other.profileChangedAt == profileChangedAt && + other.quotaSizeInBytes == quotaSizeInBytes && + other.quotaUsageInBytes == quotaUsageInBytes && + other.storageLabel == storageLabel; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (avatarColor == null ? 0 : avatarColor!.hashCode) + + (deletedAt == null ? 0 : deletedAt!.hashCode) + + (email.hashCode) + + (hasProfileImage.hashCode) + + (id.hashCode) + + (isAdmin.hashCode) + + (name.hashCode) + + (oauthId.hashCode) + + (pinCode == null ? 0 : pinCode!.hashCode) + + (profileChangedAt.hashCode) + + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + + (quotaUsageInBytes.hashCode) + + (storageLabel == null ? 0 : storageLabel!.hashCode); + + @override + String toString() => 'SyncAuthUserV1[avatarColor=$avatarColor, deletedAt=$deletedAt, email=$email, hasProfileImage=$hasProfileImage, id=$id, isAdmin=$isAdmin, name=$name, oauthId=$oauthId, pinCode=$pinCode, profileChangedAt=$profileChangedAt, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, storageLabel=$storageLabel]'; + + Map toJson() { + final json = {}; + if (this.avatarColor != null) { + json[r'avatarColor'] = this.avatarColor; + } else { + // json[r'avatarColor'] = null; + } + if (this.deletedAt != null) { + json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + } else { + // json[r'deletedAt'] = null; + } + json[r'email'] = this.email; + json[r'hasProfileImage'] = this.hasProfileImage; + json[r'id'] = this.id; + json[r'isAdmin'] = this.isAdmin; + json[r'name'] = this.name; + json[r'oauthId'] = this.oauthId; + if (this.pinCode != null) { + json[r'pinCode'] = this.pinCode; + } else { + // json[r'pinCode'] = null; + } + json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + if (this.quotaSizeInBytes != null) { + json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; + } else { + // json[r'quotaSizeInBytes'] = null; + } + json[r'quotaUsageInBytes'] = this.quotaUsageInBytes; + if (this.storageLabel != null) { + json[r'storageLabel'] = this.storageLabel; + } else { + // json[r'storageLabel'] = null; + } + return json; + } + + /// Returns a new [SyncAuthUserV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAuthUserV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAuthUserV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAuthUserV1( + avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), + deletedAt: mapDateTime(json, r'deletedAt', r''), + email: mapValueOfType(json, r'email')!, + hasProfileImage: mapValueOfType(json, r'hasProfileImage')!, + id: mapValueOfType(json, r'id')!, + isAdmin: mapValueOfType(json, r'isAdmin')!, + name: mapValueOfType(json, r'name')!, + oauthId: mapValueOfType(json, r'oauthId')!, + pinCode: mapValueOfType(json, r'pinCode'), + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), + quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes')!, + storageLabel: mapValueOfType(json, r'storageLabel'), + ); + } + 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 = SyncAuthUserV1.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 = SyncAuthUserV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAuthUserV1-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] = SyncAuthUserV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'avatarColor', + 'deletedAt', + 'email', + 'hasProfileImage', + 'id', + 'isAdmin', + 'name', + 'oauthId', + 'pinCode', + 'profileChangedAt', + 'quotaSizeInBytes', + 'quotaUsageInBytes', + 'storageLabel', + }; +} + diff --git a/mobile/openapi/lib/model/sync_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index 654ff45d6f..5368126923 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -23,45 +23,93 @@ class SyncEntityType { String toJson() => value; + static const authUserV1 = SyncEntityType._(r'AuthUserV1'); static const userV1 = SyncEntityType._(r'UserV1'); static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1'); - static const partnerV1 = SyncEntityType._(r'PartnerV1'); - static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1'); static const assetV1 = SyncEntityType._(r'AssetV1'); static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1'); static const assetExifV1 = SyncEntityType._(r'AssetExifV1'); + static const partnerV1 = SyncEntityType._(r'PartnerV1'); + static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1'); static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1'); static const partnerAssetBackfillV1 = SyncEntityType._(r'PartnerAssetBackfillV1'); static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1'); static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1'); static const partnerAssetExifBackfillV1 = SyncEntityType._(r'PartnerAssetExifBackfillV1'); + static const partnerStackBackfillV1 = SyncEntityType._(r'PartnerStackBackfillV1'); + static const partnerStackDeleteV1 = SyncEntityType._(r'PartnerStackDeleteV1'); + static const partnerStackV1 = SyncEntityType._(r'PartnerStackV1'); static const albumV1 = SyncEntityType._(r'AlbumV1'); static const albumDeleteV1 = SyncEntityType._(r'AlbumDeleteV1'); static const albumUserV1 = SyncEntityType._(r'AlbumUserV1'); static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1'); static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1'); + static const albumAssetV1 = SyncEntityType._(r'AlbumAssetV1'); + static const albumAssetBackfillV1 = SyncEntityType._(r'AlbumAssetBackfillV1'); + static const albumAssetExifV1 = SyncEntityType._(r'AlbumAssetExifV1'); + static const albumAssetExifBackfillV1 = SyncEntityType._(r'AlbumAssetExifBackfillV1'); + static const albumToAssetV1 = SyncEntityType._(r'AlbumToAssetV1'); + static const albumToAssetDeleteV1 = SyncEntityType._(r'AlbumToAssetDeleteV1'); + static const albumToAssetBackfillV1 = SyncEntityType._(r'AlbumToAssetBackfillV1'); + static const memoryV1 = SyncEntityType._(r'MemoryV1'); + static const memoryDeleteV1 = SyncEntityType._(r'MemoryDeleteV1'); + static const memoryToAssetV1 = SyncEntityType._(r'MemoryToAssetV1'); + static const memoryToAssetDeleteV1 = SyncEntityType._(r'MemoryToAssetDeleteV1'); + static const stackV1 = SyncEntityType._(r'StackV1'); + static const stackDeleteV1 = SyncEntityType._(r'StackDeleteV1'); + static const personV1 = SyncEntityType._(r'PersonV1'); + static const personDeleteV1 = SyncEntityType._(r'PersonDeleteV1'); + static const assetFaceV1 = SyncEntityType._(r'AssetFaceV1'); + static const assetFaceDeleteV1 = SyncEntityType._(r'AssetFaceDeleteV1'); + static const userMetadataV1 = SyncEntityType._(r'UserMetadataV1'); + static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1'); static const syncAckV1 = SyncEntityType._(r'SyncAckV1'); + static const syncResetV1 = SyncEntityType._(r'SyncResetV1'); /// List of all possible values in this [enum][SyncEntityType]. static const values = [ + authUserV1, userV1, userDeleteV1, - partnerV1, - partnerDeleteV1, assetV1, assetDeleteV1, assetExifV1, + partnerV1, + partnerDeleteV1, partnerAssetV1, partnerAssetBackfillV1, partnerAssetDeleteV1, partnerAssetExifV1, partnerAssetExifBackfillV1, + partnerStackBackfillV1, + partnerStackDeleteV1, + partnerStackV1, albumV1, albumDeleteV1, albumUserV1, albumUserBackfillV1, albumUserDeleteV1, + albumAssetV1, + albumAssetBackfillV1, + albumAssetExifV1, + albumAssetExifBackfillV1, + albumToAssetV1, + albumToAssetDeleteV1, + albumToAssetBackfillV1, + memoryV1, + memoryDeleteV1, + memoryToAssetV1, + memoryToAssetDeleteV1, + stackV1, + stackDeleteV1, + personV1, + personDeleteV1, + assetFaceV1, + assetFaceDeleteV1, + userMetadataV1, + userMetadataDeleteV1, syncAckV1, + syncResetV1, ]; static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value); @@ -100,24 +148,48 @@ class SyncEntityTypeTypeTransformer { SyncEntityType? decode(dynamic data, {bool allowNull = true}) { if (data != null) { switch (data) { + case r'AuthUserV1': return SyncEntityType.authUserV1; case r'UserV1': return SyncEntityType.userV1; case r'UserDeleteV1': return SyncEntityType.userDeleteV1; - case r'PartnerV1': return SyncEntityType.partnerV1; - case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1; case r'AssetV1': return SyncEntityType.assetV1; case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1; case r'AssetExifV1': return SyncEntityType.assetExifV1; + case r'PartnerV1': return SyncEntityType.partnerV1; + case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1; case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1; case r'PartnerAssetBackfillV1': return SyncEntityType.partnerAssetBackfillV1; case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1; case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1; case r'PartnerAssetExifBackfillV1': return SyncEntityType.partnerAssetExifBackfillV1; + case r'PartnerStackBackfillV1': return SyncEntityType.partnerStackBackfillV1; + case r'PartnerStackDeleteV1': return SyncEntityType.partnerStackDeleteV1; + case r'PartnerStackV1': return SyncEntityType.partnerStackV1; case r'AlbumV1': return SyncEntityType.albumV1; case r'AlbumDeleteV1': return SyncEntityType.albumDeleteV1; case r'AlbumUserV1': return SyncEntityType.albumUserV1; case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1; case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1; + case r'AlbumAssetV1': return SyncEntityType.albumAssetV1; + case r'AlbumAssetBackfillV1': return SyncEntityType.albumAssetBackfillV1; + case r'AlbumAssetExifV1': return SyncEntityType.albumAssetExifV1; + case r'AlbumAssetExifBackfillV1': return SyncEntityType.albumAssetExifBackfillV1; + case r'AlbumToAssetV1': return SyncEntityType.albumToAssetV1; + case r'AlbumToAssetDeleteV1': return SyncEntityType.albumToAssetDeleteV1; + case r'AlbumToAssetBackfillV1': return SyncEntityType.albumToAssetBackfillV1; + case r'MemoryV1': return SyncEntityType.memoryV1; + case r'MemoryDeleteV1': return SyncEntityType.memoryDeleteV1; + case r'MemoryToAssetV1': return SyncEntityType.memoryToAssetV1; + case r'MemoryToAssetDeleteV1': return SyncEntityType.memoryToAssetDeleteV1; + case r'StackV1': return SyncEntityType.stackV1; + case r'StackDeleteV1': return SyncEntityType.stackDeleteV1; + case r'PersonV1': return SyncEntityType.personV1; + case r'PersonDeleteV1': return SyncEntityType.personDeleteV1; + case r'AssetFaceV1': return SyncEntityType.assetFaceV1; + case r'AssetFaceDeleteV1': return SyncEntityType.assetFaceDeleteV1; + case r'UserMetadataV1': return SyncEntityType.userMetadataV1; + case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1; case r'SyncAckV1': return SyncEntityType.syncAckV1; + case r'SyncResetV1': return SyncEntityType.syncResetV1; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/sync_memory_asset_delete_v1.dart b/mobile/openapi/lib/model/sync_memory_asset_delete_v1.dart new file mode 100644 index 0000000000..a9af77e929 --- /dev/null +++ b/mobile/openapi/lib/model/sync_memory_asset_delete_v1.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncMemoryAssetDeleteV1 { + /// Returns a new [SyncMemoryAssetDeleteV1] instance. + SyncMemoryAssetDeleteV1({ + required this.assetId, + required this.memoryId, + }); + + String assetId; + + String memoryId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncMemoryAssetDeleteV1 && + other.assetId == assetId && + other.memoryId == memoryId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (memoryId.hashCode); + + @override + String toString() => 'SyncMemoryAssetDeleteV1[assetId=$assetId, memoryId=$memoryId]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'memoryId'] = this.memoryId; + return json; + } + + /// Returns a new [SyncMemoryAssetDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncMemoryAssetDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncMemoryAssetDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncMemoryAssetDeleteV1( + assetId: mapValueOfType(json, r'assetId')!, + memoryId: mapValueOfType(json, r'memoryId')!, + ); + } + 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 = SyncMemoryAssetDeleteV1.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 = SyncMemoryAssetDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncMemoryAssetDeleteV1-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] = SyncMemoryAssetDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'memoryId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_memory_asset_v1.dart b/mobile/openapi/lib/model/sync_memory_asset_v1.dart new file mode 100644 index 0000000000..d26e3c9a29 --- /dev/null +++ b/mobile/openapi/lib/model/sync_memory_asset_v1.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncMemoryAssetV1 { + /// Returns a new [SyncMemoryAssetV1] instance. + SyncMemoryAssetV1({ + required this.assetId, + required this.memoryId, + }); + + String assetId; + + String memoryId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncMemoryAssetV1 && + other.assetId == assetId && + other.memoryId == memoryId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (memoryId.hashCode); + + @override + String toString() => 'SyncMemoryAssetV1[assetId=$assetId, memoryId=$memoryId]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'memoryId'] = this.memoryId; + return json; + } + + /// Returns a new [SyncMemoryAssetV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncMemoryAssetV1? fromJson(dynamic value) { + upgradeDto(value, "SyncMemoryAssetV1"); + if (value is Map) { + final json = value.cast(); + + return SyncMemoryAssetV1( + assetId: mapValueOfType(json, r'assetId')!, + memoryId: mapValueOfType(json, r'memoryId')!, + ); + } + 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 = SyncMemoryAssetV1.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 = SyncMemoryAssetV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncMemoryAssetV1-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] = SyncMemoryAssetV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'memoryId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_memory_delete_v1.dart b/mobile/openapi/lib/model/sync_memory_delete_v1.dart new file mode 100644 index 0000000000..9702da5aaf --- /dev/null +++ b/mobile/openapi/lib/model/sync_memory_delete_v1.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncMemoryDeleteV1 { + /// Returns a new [SyncMemoryDeleteV1] instance. + SyncMemoryDeleteV1({ + required this.memoryId, + }); + + String memoryId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncMemoryDeleteV1 && + other.memoryId == memoryId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (memoryId.hashCode); + + @override + String toString() => 'SyncMemoryDeleteV1[memoryId=$memoryId]'; + + Map toJson() { + final json = {}; + json[r'memoryId'] = this.memoryId; + return json; + } + + /// Returns a new [SyncMemoryDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncMemoryDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncMemoryDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncMemoryDeleteV1( + memoryId: mapValueOfType(json, r'memoryId')!, + ); + } + 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 = SyncMemoryDeleteV1.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 = SyncMemoryDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncMemoryDeleteV1-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] = SyncMemoryDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'memoryId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_memory_v1.dart b/mobile/openapi/lib/model/sync_memory_v1.dart new file mode 100644 index 0000000000..2ae2b01fd7 --- /dev/null +++ b/mobile/openapi/lib/model/sync_memory_v1.dart @@ -0,0 +1,203 @@ +// +// 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 SyncMemoryV1 { + /// Returns a new [SyncMemoryV1] instance. + SyncMemoryV1({ + required this.createdAt, + required this.data, + required this.deletedAt, + required this.hideAt, + required this.id, + required this.isSaved, + required this.memoryAt, + required this.ownerId, + required this.seenAt, + required this.showAt, + required this.type, + required this.updatedAt, + }); + + DateTime createdAt; + + Object data; + + DateTime? deletedAt; + + DateTime? hideAt; + + String id; + + bool isSaved; + + DateTime memoryAt; + + String ownerId; + + DateTime? seenAt; + + DateTime? showAt; + + MemoryType type; + + DateTime updatedAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncMemoryV1 && + other.createdAt == createdAt && + other.data == data && + other.deletedAt == deletedAt && + other.hideAt == hideAt && + other.id == id && + other.isSaved == isSaved && + other.memoryAt == memoryAt && + other.ownerId == ownerId && + other.seenAt == seenAt && + other.showAt == showAt && + other.type == type && + other.updatedAt == updatedAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (createdAt.hashCode) + + (data.hashCode) + + (deletedAt == null ? 0 : deletedAt!.hashCode) + + (hideAt == null ? 0 : hideAt!.hashCode) + + (id.hashCode) + + (isSaved.hashCode) + + (memoryAt.hashCode) + + (ownerId.hashCode) + + (seenAt == null ? 0 : seenAt!.hashCode) + + (showAt == null ? 0 : showAt!.hashCode) + + (type.hashCode) + + (updatedAt.hashCode); + + @override + String toString() => 'SyncMemoryV1[createdAt=$createdAt, data=$data, deletedAt=$deletedAt, hideAt=$hideAt, id=$id, isSaved=$isSaved, memoryAt=$memoryAt, ownerId=$ownerId, seenAt=$seenAt, showAt=$showAt, type=$type, updatedAt=$updatedAt]'; + + Map toJson() { + final json = {}; + json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'data'] = this.data; + if (this.deletedAt != null) { + json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + } else { + // json[r'deletedAt'] = null; + } + if (this.hideAt != null) { + json[r'hideAt'] = 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'ownerId'] = this.ownerId; + if (this.seenAt != null) { + json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + } else { + // json[r'seenAt'] = null; + } + if (this.showAt != null) { + json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + } else { + // json[r'showAt'] = null; + } + json[r'type'] = this.type; + json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + return json; + } + + /// Returns a new [SyncMemoryV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncMemoryV1? fromJson(dynamic value) { + upgradeDto(value, "SyncMemoryV1"); + if (value is Map) { + 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''), + id: mapValueOfType(json, r'id')!, + isSaved: mapValueOfType(json, r'isSaved')!, + memoryAt: mapDateTime(json, r'memoryAt', r'')!, + ownerId: mapValueOfType(json, r'ownerId')!, + seenAt: mapDateTime(json, r'seenAt', r''), + showAt: mapDateTime(json, r'showAt', r''), + type: MemoryType.fromJson(json[r'type'])!, + updatedAt: mapDateTime(json, r'updatedAt', r'')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SyncMemoryV1.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 = SyncMemoryV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncMemoryV1-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] = SyncMemoryV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'createdAt', + 'data', + 'deletedAt', + 'hideAt', + 'id', + 'isSaved', + 'memoryAt', + 'ownerId', + 'seenAt', + 'showAt', + 'type', + 'updatedAt', + }; +} + diff --git a/mobile/openapi/lib/model/sync_person_delete_v1.dart b/mobile/openapi/lib/model/sync_person_delete_v1.dart new file mode 100644 index 0000000000..002f5c5b83 --- /dev/null +++ b/mobile/openapi/lib/model/sync_person_delete_v1.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncPersonDeleteV1 { + /// Returns a new [SyncPersonDeleteV1] instance. + SyncPersonDeleteV1({ + required this.personId, + }); + + String personId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncPersonDeleteV1 && + other.personId == personId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (personId.hashCode); + + @override + String toString() => 'SyncPersonDeleteV1[personId=$personId]'; + + Map toJson() { + final json = {}; + json[r'personId'] = this.personId; + return json; + } + + /// Returns a new [SyncPersonDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncPersonDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncPersonDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncPersonDeleteV1( + personId: mapValueOfType(json, r'personId')!, + ); + } + 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 = SyncPersonDeleteV1.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 = SyncPersonDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncPersonDeleteV1-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] = SyncPersonDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'personId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_person_v1.dart b/mobile/openapi/lib/model/sync_person_v1.dart new file mode 100644 index 0000000000..6749beb3e1 --- /dev/null +++ b/mobile/openapi/lib/model/sync_person_v1.dart @@ -0,0 +1,183 @@ +// +// 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 SyncPersonV1 { + /// Returns a new [SyncPersonV1] instance. + SyncPersonV1({ + required this.birthDate, + required this.color, + required this.createdAt, + required this.faceAssetId, + required this.id, + required this.isFavorite, + required this.isHidden, + required this.name, + required this.ownerId, + required this.updatedAt, + }); + + DateTime? birthDate; + + String? color; + + DateTime createdAt; + + String? faceAssetId; + + String id; + + bool isFavorite; + + bool isHidden; + + String name; + + String ownerId; + + DateTime updatedAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncPersonV1 && + other.birthDate == birthDate && + other.color == color && + other.createdAt == createdAt && + other.faceAssetId == faceAssetId && + other.id == id && + other.isFavorite == isFavorite && + other.isHidden == isHidden && + other.name == name && + other.ownerId == ownerId && + other.updatedAt == updatedAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (birthDate == null ? 0 : birthDate!.hashCode) + + (color == null ? 0 : color!.hashCode) + + (createdAt.hashCode) + + (faceAssetId == null ? 0 : faceAssetId!.hashCode) + + (id.hashCode) + + (isFavorite.hashCode) + + (isHidden.hashCode) + + (name.hashCode) + + (ownerId.hashCode) + + (updatedAt.hashCode); + + @override + String toString() => 'SyncPersonV1[birthDate=$birthDate, color=$color, createdAt=$createdAt, faceAssetId=$faceAssetId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, ownerId=$ownerId, updatedAt=$updatedAt]'; + + Map toJson() { + final json = {}; + if (this.birthDate != null) { + json[r'birthDate'] = this.birthDate!.toUtc().toIso8601String(); + } else { + // json[r'birthDate'] = null; + } + if (this.color != null) { + json[r'color'] = this.color; + } else { + // json[r'color'] = null; + } + json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + if (this.faceAssetId != null) { + json[r'faceAssetId'] = this.faceAssetId; + } else { + // json[r'faceAssetId'] = null; + } + json[r'id'] = this.id; + json[r'isFavorite'] = this.isFavorite; + json[r'isHidden'] = this.isHidden; + json[r'name'] = this.name; + json[r'ownerId'] = this.ownerId; + json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + return json; + } + + /// Returns a new [SyncPersonV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncPersonV1? fromJson(dynamic value) { + upgradeDto(value, "SyncPersonV1"); + if (value is Map) { + final json = value.cast(); + + return SyncPersonV1( + birthDate: mapDateTime(json, r'birthDate', r''), + color: mapValueOfType(json, r'color'), + createdAt: mapDateTime(json, r'createdAt', r'')!, + 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'')!, + ); + } + 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 = SyncPersonV1.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 = SyncPersonV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncPersonV1-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] = SyncPersonV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'birthDate', + 'color', + 'createdAt', + 'faceAssetId', + 'id', + 'isFavorite', + 'isHidden', + 'name', + 'ownerId', + 'updatedAt', + }; +} + diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index c149c329de..8a1857366e 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -23,25 +23,47 @@ class SyncRequestType { String toJson() => value; - static const usersV1 = SyncRequestType._(r'UsersV1'); - static const partnersV1 = SyncRequestType._(r'PartnersV1'); - static const assetsV1 = SyncRequestType._(r'AssetsV1'); - static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1'); - static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1'); - static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1'); static const albumsV1 = SyncRequestType._(r'AlbumsV1'); static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1'); + static const albumToAssetsV1 = SyncRequestType._(r'AlbumToAssetsV1'); + static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1'); + static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1'); + static const assetsV1 = SyncRequestType._(r'AssetsV1'); + static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1'); + static const authUsersV1 = SyncRequestType._(r'AuthUsersV1'); + static const memoriesV1 = SyncRequestType._(r'MemoriesV1'); + static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1'); + static const partnersV1 = SyncRequestType._(r'PartnersV1'); + static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1'); + static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1'); + static const partnerStacksV1 = SyncRequestType._(r'PartnerStacksV1'); + static const stacksV1 = SyncRequestType._(r'StacksV1'); + static const usersV1 = SyncRequestType._(r'UsersV1'); + static const peopleV1 = SyncRequestType._(r'PeopleV1'); + static const assetFacesV1 = SyncRequestType._(r'AssetFacesV1'); + static const userMetadataV1 = SyncRequestType._(r'UserMetadataV1'); /// List of all possible values in this [enum][SyncRequestType]. static const values = [ - usersV1, - partnersV1, - assetsV1, - assetExifsV1, - partnerAssetsV1, - partnerAssetExifsV1, albumsV1, albumUsersV1, + albumToAssetsV1, + albumAssetsV1, + albumAssetExifsV1, + assetsV1, + assetExifsV1, + authUsersV1, + memoriesV1, + memoryToAssetsV1, + partnersV1, + partnerAssetsV1, + partnerAssetExifsV1, + partnerStacksV1, + stacksV1, + usersV1, + peopleV1, + assetFacesV1, + userMetadataV1, ]; static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value); @@ -80,14 +102,25 @@ class SyncRequestTypeTypeTransformer { SyncRequestType? decode(dynamic data, {bool allowNull = true}) { if (data != null) { switch (data) { - case r'UsersV1': return SyncRequestType.usersV1; - case r'PartnersV1': return SyncRequestType.partnersV1; - case r'AssetsV1': return SyncRequestType.assetsV1; - case r'AssetExifsV1': return SyncRequestType.assetExifsV1; - case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1; - case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1; case r'AlbumsV1': return SyncRequestType.albumsV1; case r'AlbumUsersV1': return SyncRequestType.albumUsersV1; + case r'AlbumToAssetsV1': return SyncRequestType.albumToAssetsV1; + case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1; + case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1; + case r'AssetsV1': return SyncRequestType.assetsV1; + case r'AssetExifsV1': return SyncRequestType.assetExifsV1; + case r'AuthUsersV1': return SyncRequestType.authUsersV1; + case r'MemoriesV1': return SyncRequestType.memoriesV1; + case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1; + case r'PartnersV1': return SyncRequestType.partnersV1; + case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1; + case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1; + case r'PartnerStacksV1': return SyncRequestType.partnerStacksV1; + case r'StacksV1': return SyncRequestType.stacksV1; + case r'UsersV1': return SyncRequestType.usersV1; + case r'PeopleV1': return SyncRequestType.peopleV1; + case r'AssetFacesV1': return SyncRequestType.assetFacesV1; + case r'UserMetadataV1': return SyncRequestType.userMetadataV1; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/sync_stack_delete_v1.dart b/mobile/openapi/lib/model/sync_stack_delete_v1.dart new file mode 100644 index 0000000000..22c6d99a52 --- /dev/null +++ b/mobile/openapi/lib/model/sync_stack_delete_v1.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncStackDeleteV1 { + /// Returns a new [SyncStackDeleteV1] instance. + SyncStackDeleteV1({ + required this.stackId, + }); + + String stackId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncStackDeleteV1 && + other.stackId == stackId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (stackId.hashCode); + + @override + String toString() => 'SyncStackDeleteV1[stackId=$stackId]'; + + Map toJson() { + final json = {}; + json[r'stackId'] = this.stackId; + return json; + } + + /// Returns a new [SyncStackDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncStackDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncStackDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncStackDeleteV1( + stackId: mapValueOfType(json, r'stackId')!, + ); + } + 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 = SyncStackDeleteV1.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 = SyncStackDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncStackDeleteV1-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] = SyncStackDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'stackId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_stack_v1.dart b/mobile/openapi/lib/model/sync_stack_v1.dart new file mode 100644 index 0000000000..c65affe8c0 --- /dev/null +++ b/mobile/openapi/lib/model/sync_stack_v1.dart @@ -0,0 +1,131 @@ +// +// 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 SyncStackV1 { + /// Returns a new [SyncStackV1] instance. + SyncStackV1({ + required this.createdAt, + required this.id, + required this.ownerId, + required this.primaryAssetId, + required this.updatedAt, + }); + + DateTime createdAt; + + String id; + + String ownerId; + + String primaryAssetId; + + DateTime updatedAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncStackV1 && + other.createdAt == createdAt && + other.id == id && + other.ownerId == ownerId && + other.primaryAssetId == primaryAssetId && + other.updatedAt == updatedAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (createdAt.hashCode) + + (id.hashCode) + + (ownerId.hashCode) + + (primaryAssetId.hashCode) + + (updatedAt.hashCode); + + @override + String toString() => 'SyncStackV1[createdAt=$createdAt, id=$id, ownerId=$ownerId, primaryAssetId=$primaryAssetId, updatedAt=$updatedAt]'; + + Map toJson() { + final json = {}; + json[r'createdAt'] = 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(); + return json; + } + + /// Returns a new [SyncStackV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncStackV1? fromJson(dynamic value) { + upgradeDto(value, "SyncStackV1"); + if (value is Map) { + final json = value.cast(); + + return SyncStackV1( + createdAt: mapDateTime(json, r'createdAt', r'')!, + id: mapValueOfType(json, r'id')!, + ownerId: mapValueOfType(json, r'ownerId')!, + primaryAssetId: mapValueOfType(json, r'primaryAssetId')!, + updatedAt: mapDateTime(json, r'updatedAt', r'')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SyncStackV1.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 = SyncStackV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncStackV1-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] = SyncStackV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'createdAt', + 'id', + 'ownerId', + 'primaryAssetId', + 'updatedAt', + }; +} + diff --git a/mobile/openapi/lib/model/sync_stream_dto.dart b/mobile/openapi/lib/model/sync_stream_dto.dart index 28fd3dfaee..9884eef342 100644 --- a/mobile/openapi/lib/model/sync_stream_dto.dart +++ b/mobile/openapi/lib/model/sync_stream_dto.dart @@ -13,25 +13,41 @@ part of openapi.api; class SyncStreamDto { /// Returns a new [SyncStreamDto] instance. SyncStreamDto({ + this.reset, this.types = const [], }); + /// + /// 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? reset; + List types; @override bool operator ==(Object other) => identical(this, other) || other is SyncStreamDto && + other.reset == reset && _deepEquality.equals(other.types, types); @override int get hashCode => // ignore: unnecessary_parenthesis + (reset == null ? 0 : reset!.hashCode) + (types.hashCode); @override - String toString() => 'SyncStreamDto[types=$types]'; + String toString() => 'SyncStreamDto[reset=$reset, types=$types]'; Map toJson() { final json = {}; + if (this.reset != null) { + json[r'reset'] = this.reset; + } else { + // json[r'reset'] = null; + } json[r'types'] = this.types; return json; } @@ -45,6 +61,7 @@ class SyncStreamDto { final json = value.cast(); return SyncStreamDto( + reset: mapValueOfType(json, r'reset'), types: SyncRequestType.listFromJson(json[r'types']), ); } diff --git a/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart new file mode 100644 index 0000000000..f39acc617b --- /dev/null +++ b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncUserMetadataDeleteV1 { + /// Returns a new [SyncUserMetadataDeleteV1] instance. + SyncUserMetadataDeleteV1({ + required this.key, + required this.userId, + }); + + UserMetadataKey key; + + String userId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncUserMetadataDeleteV1 && + other.key == key && + other.userId == userId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (key.hashCode) + + (userId.hashCode); + + @override + String toString() => 'SyncUserMetadataDeleteV1[key=$key, userId=$userId]'; + + Map toJson() { + final json = {}; + json[r'key'] = this.key; + json[r'userId'] = this.userId; + return json; + } + + /// Returns a new [SyncUserMetadataDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncUserMetadataDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncUserMetadataDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncUserMetadataDeleteV1( + key: UserMetadataKey.fromJson(json[r'key'])!, + 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 = SyncUserMetadataDeleteV1.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 = SyncUserMetadataDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncUserMetadataDeleteV1-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] = SyncUserMetadataDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'key', + 'userId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_user_metadata_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_v1.dart new file mode 100644 index 0000000000..cf39b6d960 --- /dev/null +++ b/mobile/openapi/lib/model/sync_user_metadata_v1.dart @@ -0,0 +1,115 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncUserMetadataV1 { + /// Returns a new [SyncUserMetadataV1] instance. + SyncUserMetadataV1({ + required this.key, + required this.userId, + required this.value, + }); + + UserMetadataKey key; + + String userId; + + Object value; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncUserMetadataV1 && + other.key == key && + other.userId == userId && + other.value == value; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (key.hashCode) + + (userId.hashCode) + + (value.hashCode); + + @override + String toString() => 'SyncUserMetadataV1[key=$key, userId=$userId, value=$value]'; + + Map toJson() { + final json = {}; + json[r'key'] = this.key; + json[r'userId'] = this.userId; + json[r'value'] = this.value; + return json; + } + + /// Returns a new [SyncUserMetadataV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncUserMetadataV1? fromJson(dynamic value) { + upgradeDto(value, "SyncUserMetadataV1"); + if (value is Map) { + final json = value.cast(); + + return SyncUserMetadataV1( + key: UserMetadataKey.fromJson(json[r'key'])!, + userId: mapValueOfType(json, r'userId')!, + value: mapValueOfType(json, r'value')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SyncUserMetadataV1.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 = SyncUserMetadataV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncUserMetadataV1-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] = SyncUserMetadataV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'key', + 'userId', + 'value', + }; +} + diff --git a/mobile/openapi/lib/model/sync_user_v1.dart b/mobile/openapi/lib/model/sync_user_v1.dart index b9b41bb723..c01ddcc9fc 100644 --- a/mobile/openapi/lib/model/sync_user_v1.dart +++ b/mobile/openapi/lib/model/sync_user_v1.dart @@ -13,12 +13,15 @@ part of openapi.api; class SyncUserV1 { /// Returns a new [SyncUserV1] instance. SyncUserV1({ + required this.avatarColor, required this.deletedAt, required this.email, required this.id, required this.name, }); + UserAvatarColor? avatarColor; + DateTime? deletedAt; String email; @@ -29,6 +32,7 @@ class SyncUserV1 { @override bool operator ==(Object other) => identical(this, other) || other is SyncUserV1 && + other.avatarColor == avatarColor && other.deletedAt == deletedAt && other.email == email && other.id == id && @@ -37,16 +41,22 @@ class SyncUserV1 { @override int get hashCode => // ignore: unnecessary_parenthesis + (avatarColor == null ? 0 : avatarColor!.hashCode) + (deletedAt == null ? 0 : deletedAt!.hashCode) + (email.hashCode) + (id.hashCode) + (name.hashCode); @override - String toString() => 'SyncUserV1[deletedAt=$deletedAt, email=$email, id=$id, name=$name]'; + String toString() => 'SyncUserV1[avatarColor=$avatarColor, deletedAt=$deletedAt, email=$email, id=$id, name=$name]'; Map toJson() { final json = {}; + if (this.avatarColor != null) { + json[r'avatarColor'] = this.avatarColor; + } else { + // json[r'avatarColor'] = null; + } if (this.deletedAt != null) { json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); } else { @@ -67,6 +77,7 @@ class SyncUserV1 { final json = value.cast(); return SyncUserV1( + avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), deletedAt: mapDateTime(json, r'deletedAt', r''), email: mapValueOfType(json, r'email')!, id: mapValueOfType(json, r'id')!, @@ -118,6 +129,7 @@ class SyncUserV1 { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'avatarColor', 'deletedAt', 'email', 'id', diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index 59d5f09fc9..38dbb30f0c 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -23,6 +23,7 @@ class SystemConfigDto { required this.map, required this.metadata, required this.newVersionCheck, + required this.nightlyTasks, required this.notifications, required this.oauth, required this.passwordLogin, @@ -55,6 +56,8 @@ class SystemConfigDto { SystemConfigNewVersionCheckDto newVersionCheck; + SystemConfigNightlyTasksDto nightlyTasks; + SystemConfigNotificationsDto notifications; SystemConfigOAuthDto oauth; @@ -87,6 +90,7 @@ class SystemConfigDto { other.map == map && other.metadata == metadata && other.newVersionCheck == newVersionCheck && + other.nightlyTasks == nightlyTasks && other.notifications == notifications && other.oauth == oauth && other.passwordLogin == passwordLogin && @@ -111,6 +115,7 @@ class SystemConfigDto { (map.hashCode) + (metadata.hashCode) + (newVersionCheck.hashCode) + + (nightlyTasks.hashCode) + (notifications.hashCode) + (oauth.hashCode) + (passwordLogin.hashCode) + @@ -123,7 +128,7 @@ class SystemConfigDto { (user.hashCode); @override - String toString() => 'SystemConfigDto[backup=$backup, ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, templates=$templates, theme=$theme, trash=$trash, user=$user]'; + String toString() => 'SystemConfigDto[backup=$backup, ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, nightlyTasks=$nightlyTasks, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, templates=$templates, theme=$theme, trash=$trash, user=$user]'; Map toJson() { final json = {}; @@ -137,6 +142,7 @@ class SystemConfigDto { json[r'map'] = this.map; json[r'metadata'] = this.metadata; json[r'newVersionCheck'] = this.newVersionCheck; + json[r'nightlyTasks'] = this.nightlyTasks; json[r'notifications'] = this.notifications; json[r'oauth'] = this.oauth; json[r'passwordLogin'] = this.passwordLogin; @@ -169,6 +175,7 @@ class SystemConfigDto { map: SystemConfigMapDto.fromJson(json[r'map'])!, metadata: SystemConfigMetadataDto.fromJson(json[r'metadata'])!, newVersionCheck: SystemConfigNewVersionCheckDto.fromJson(json[r'newVersionCheck'])!, + nightlyTasks: SystemConfigNightlyTasksDto.fromJson(json[r'nightlyTasks'])!, notifications: SystemConfigNotificationsDto.fromJson(json[r'notifications'])!, oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!, passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!, @@ -236,6 +243,7 @@ class SystemConfigDto { 'map', 'metadata', 'newVersionCheck', + 'nightlyTasks', 'notifications', 'oauth', 'passwordLogin', diff --git a/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart new file mode 100644 index 0000000000..ab7b4b37c2 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart @@ -0,0 +1,139 @@ +// +// 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 SystemConfigNightlyTasksDto { + /// Returns a new [SystemConfigNightlyTasksDto] instance. + SystemConfigNightlyTasksDto({ + required this.clusterNewFaces, + required this.databaseCleanup, + required this.generateMemories, + required this.missingThumbnails, + required this.startTime, + required this.syncQuotaUsage, + }); + + bool clusterNewFaces; + + bool databaseCleanup; + + bool generateMemories; + + bool missingThumbnails; + + String startTime; + + bool syncQuotaUsage; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigNightlyTasksDto && + other.clusterNewFaces == clusterNewFaces && + other.databaseCleanup == databaseCleanup && + other.generateMemories == generateMemories && + other.missingThumbnails == missingThumbnails && + other.startTime == startTime && + other.syncQuotaUsage == syncQuotaUsage; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (clusterNewFaces.hashCode) + + (databaseCleanup.hashCode) + + (generateMemories.hashCode) + + (missingThumbnails.hashCode) + + (startTime.hashCode) + + (syncQuotaUsage.hashCode); + + @override + String toString() => 'SystemConfigNightlyTasksDto[clusterNewFaces=$clusterNewFaces, databaseCleanup=$databaseCleanup, generateMemories=$generateMemories, missingThumbnails=$missingThumbnails, startTime=$startTime, syncQuotaUsage=$syncQuotaUsage]'; + + Map toJson() { + final json = {}; + json[r'clusterNewFaces'] = this.clusterNewFaces; + json[r'databaseCleanup'] = this.databaseCleanup; + json[r'generateMemories'] = this.generateMemories; + json[r'missingThumbnails'] = this.missingThumbnails; + json[r'startTime'] = this.startTime; + json[r'syncQuotaUsage'] = this.syncQuotaUsage; + return json; + } + + /// Returns a new [SystemConfigNightlyTasksDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigNightlyTasksDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigNightlyTasksDto"); + if (value is Map) { + final json = value.cast(); + + return SystemConfigNightlyTasksDto( + clusterNewFaces: mapValueOfType(json, r'clusterNewFaces')!, + databaseCleanup: mapValueOfType(json, r'databaseCleanup')!, + generateMemories: mapValueOfType(json, r'generateMemories')!, + missingThumbnails: mapValueOfType(json, r'missingThumbnails')!, + startTime: mapValueOfType(json, r'startTime')!, + syncQuotaUsage: mapValueOfType(json, r'syncQuotaUsage')!, + ); + } + 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 = SystemConfigNightlyTasksDto.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 = SystemConfigNightlyTasksDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigNightlyTasksDto-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] = SystemConfigNightlyTasksDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'clusterNewFaces', + 'databaseCleanup', + 'generateMemories', + 'missingThumbnails', + 'startTime', + 'syncQuotaUsage', + }; +} + 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 e100e8e5ca..c8f91be1f1 100644 --- a/mobile/openapi/lib/model/system_config_o_auth_dto.dart +++ b/mobile/openapi/lib/model/system_config_o_auth_dto.dart @@ -24,6 +24,7 @@ class SystemConfigOAuthDto { required this.mobileOverrideEnabled, required this.mobileRedirectUri, required this.profileSigningAlgorithm, + required this.roleClaim, required this.scope, required this.signingAlgorithm, required this.storageLabelClaim, @@ -55,6 +56,8 @@ class SystemConfigOAuthDto { String profileSigningAlgorithm; + String roleClaim; + String scope; String signingAlgorithm; @@ -81,6 +84,7 @@ class SystemConfigOAuthDto { other.mobileOverrideEnabled == mobileOverrideEnabled && other.mobileRedirectUri == mobileRedirectUri && other.profileSigningAlgorithm == profileSigningAlgorithm && + other.roleClaim == roleClaim && other.scope == scope && other.signingAlgorithm == signingAlgorithm && other.storageLabelClaim == storageLabelClaim && @@ -102,6 +106,7 @@ class SystemConfigOAuthDto { (mobileOverrideEnabled.hashCode) + (mobileRedirectUri.hashCode) + (profileSigningAlgorithm.hashCode) + + (roleClaim.hashCode) + (scope.hashCode) + (signingAlgorithm.hashCode) + (storageLabelClaim.hashCode) + @@ -110,7 +115,7 @@ 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, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]'; + 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]'; Map toJson() { final json = {}; @@ -129,6 +134,7 @@ class SystemConfigOAuthDto { json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled; json[r'mobileRedirectUri'] = this.mobileRedirectUri; json[r'profileSigningAlgorithm'] = this.profileSigningAlgorithm; + json[r'roleClaim'] = this.roleClaim; json[r'scope'] = this.scope; json[r'signingAlgorithm'] = this.signingAlgorithm; json[r'storageLabelClaim'] = this.storageLabelClaim; @@ -158,6 +164,7 @@ class SystemConfigOAuthDto { mobileOverrideEnabled: mapValueOfType(json, r'mobileOverrideEnabled')!, mobileRedirectUri: mapValueOfType(json, r'mobileRedirectUri')!, profileSigningAlgorithm: mapValueOfType(json, r'profileSigningAlgorithm')!, + roleClaim: mapValueOfType(json, r'roleClaim')!, scope: mapValueOfType(json, r'scope')!, signingAlgorithm: mapValueOfType(json, r'signingAlgorithm')!, storageLabelClaim: mapValueOfType(json, r'storageLabelClaim')!, @@ -222,6 +229,7 @@ class SystemConfigOAuthDto { 'mobileOverrideEnabled', 'mobileRedirectUri', 'profileSigningAlgorithm', + 'roleClaim', 'scope', 'signingAlgorithm', 'storageLabelClaim', diff --git a/mobile/openapi/lib/model/user_metadata_key.dart b/mobile/openapi/lib/model/user_metadata_key.dart new file mode 100644 index 0000000000..845b5ae9bb --- /dev/null +++ b/mobile/openapi/lib/model/user_metadata_key.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; + + +class UserMetadataKey { + /// Instantiate a new enum with the provided [value]. + const UserMetadataKey._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const preferences = UserMetadataKey._(r'preferences'); + static const license = UserMetadataKey._(r'license'); + static const onboarding = UserMetadataKey._(r'onboarding'); + + /// List of all possible values in this [enum][UserMetadataKey]. + static const values = [ + preferences, + license, + onboarding, + ]; + + static UserMetadataKey? fromJson(dynamic value) => UserMetadataKeyTypeTransformer().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 = UserMetadataKey.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [UserMetadataKey] to String, +/// and [decode] dynamic data back to [UserMetadataKey]. +class UserMetadataKeyTypeTransformer { + factory UserMetadataKeyTypeTransformer() => _instance ??= const UserMetadataKeyTypeTransformer._(); + + const UserMetadataKeyTypeTransformer._(); + + String encode(UserMetadataKey data) => data.value; + + /// Decodes a [dynamic value][data] to a UserMetadataKey. + /// + /// 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. + UserMetadataKey? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'preferences': return UserMetadataKey.preferences; + case r'license': return UserMetadataKey.license; + case r'onboarding': return UserMetadataKey.onboarding; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [UserMetadataKeyTypeTransformer] instance. + static UserMetadataKeyTypeTransformer? _instance; +} + diff --git a/mobile/pigeon/native_sync_api.dart b/mobile/pigeon/native_sync_api.dart index 33e2429405..4f14b7a0b9 100644 --- a/mobile/pigeon/native_sync_api.dart +++ b/mobile/pigeon/native_sync_api.dart @@ -23,6 +23,7 @@ class PlatformAsset { final int? width; final int? height; final int durationInSeconds; + final int orientation; const PlatformAsset({ required this.id, @@ -33,6 +34,7 @@ class PlatformAsset { this.width, this.height, this.durationInSeconds = 0, + this.orientation = 0, }); } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 3be12d497c..20154649ea 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" auto_route: dependency: "direct main" description: @@ -509,10 +509,10 @@ packages: dependency: "direct dev" description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -1007,10 +1007,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -1064,10 +1064,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -1689,6 +1689,14 @@ packages: description: flutter source: sdk version: "0.0.0" + sliver_tools: + dependency: "direct main" + description: + name: sliver_tools + sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 + url: "https://pub.dev" + source: hosted + version: "0.2.12" socket_io_client: dependency: "direct main" description: @@ -2029,10 +2037,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" wakelock_plus: dependency: "direct main" description: @@ -2085,10 +2093,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.1.0" win32: dependency: transitive description: @@ -2155,4 +2163,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.7.0 <4.0.0" - flutter: ">=3.29.3" + flutter: ">=3.32.8" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 56aa09e1df..cfec68689d 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,11 +2,11 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.135.3+204 +version: 1.136.0+3000 environment: sdk: '>=3.3.0 <4.0.0' - flutter: 3.29.3 + flutter: 3.32.8 isar_version: &isar_version 3.1.8 @@ -43,7 +43,7 @@ dependencies: home_widget: ^0.8.0 http: ^1.3.0 image_picker: ^1.1.2 - intl: ^0.19.0 + intl: ^0.20.0 local_auth: ^2.3.0 logging: ^1.3.0 maplibre_gl: ^0.22.0 @@ -63,6 +63,7 @@ dependencies: scrollable_positioned_list: ^0.3.8 share_handler: ^0.0.22 share_plus: ^10.1.4 + sliver_tools: ^0.2.12 socket_io_client: ^2.0.3+1 stream_transform: ^2.1.1 thumbhash: 0.1.0+1 @@ -139,7 +140,6 @@ flutter: - family: OverpassMono fonts: - asset: fonts/overpass/OverpassMono.ttf - flutter_launcher_icons: image_path_android: 'assets/immich-logo.png' adaptive_icon_background: '#ffffff' diff --git a/mobile/test/domain/service.mock.dart b/mobile/test/domain/service.mock.dart index f4c5a32a4b..8293faf125 100644 --- a/mobile/test/domain/service.mock.dart +++ b/mobile/test/domain/service.mock.dart @@ -2,6 +2,8 @@ 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/upload.service.dart'; import 'package:mocktail/mocktail.dart'; class MockStoreService extends Mock implements StoreService {} @@ -11,3 +13,7 @@ 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 MockUploadService extends Mock implements UploadService {} diff --git a/mobile/test/domain/services/hash_service_test.dart b/mobile/test/domain/services/hash_service_test.dart index 623aed5409..262766662b 100644 --- a/mobile/test/domain/services/hash_service_test.dart +++ b/mobile/test/domain/services/hash_service_test.dart @@ -3,9 +3,9 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/hash.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:mocktail/mocktail.dart'; import '../../fixtures/album.stub.dart'; @@ -50,8 +50,7 @@ void main() { when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer( (_) async => [LocalAlbumStub.recent.copyWith(assetCount: 0)], ); - when(() => mockAlbumRepo.getAssetsToHash(LocalAlbumStub.recent.id)) - .thenAnswer((_) async => []); + when(() => mockAlbumRepo.getAssetsToHash(LocalAlbumStub.recent.id)).thenAnswer((_) async => []); await sut.hashAssets(); @@ -64,12 +63,9 @@ void main() { test('skips assets without files', () async { final album = LocalAlbumStub.recent; final asset = LocalAssetStub.image1; - when(() => mockAlbumRepo.getAll(sortBy: sortBy)) - .thenAnswer((_) async => [album]); - when(() => mockAlbumRepo.getAssetsToHash(album.id)) - .thenAnswer((_) async => [asset]); - when(() => mockStorageRepo.getFileForAsset(asset)) - .thenAnswer((_) async => null); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset]); + when(() => mockStorageRepo.getFileForAsset(asset.id)).thenAnswer((_) async => null); await sut.hashAssets(); @@ -85,12 +81,9 @@ void main() { when(() => mockFile.length()).thenAnswer((_) async => 1000); when(() => mockFile.path).thenReturn('image-path'); - when(() => mockAlbumRepo.getAll(sortBy: sortBy)) - .thenAnswer((_) async => [album]); - when(() => mockAlbumRepo.getAssetsToHash(album.id)) - .thenAnswer((_) async => [asset]); - when(() => mockStorageRepo.getFileForAsset(asset)) - .thenAnswer((_) async => mockFile); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset]); + when(() => mockStorageRepo.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); when(() => mockNativeApi.hashPaths(['image-path'])).thenAnswer( (_) async => [hash], ); @@ -98,9 +91,7 @@ void main() { await sut.hashAssets(); verify(() => mockNativeApi.hashPaths(['image-path'])).called(1); - final captured = verify(() => mockAssetRepo.updateHashes(captureAny())) - .captured - .first as List; + final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List; expect(captured.length, 1); expect(captured[0].checksum, base64.encode(hash)); }); @@ -112,21 +103,15 @@ void main() { when(() => mockFile.length()).thenAnswer((_) async => 1000); when(() => mockFile.path).thenReturn('image-path'); - when(() => mockAlbumRepo.getAll(sortBy: sortBy)) - .thenAnswer((_) async => [album]); - when(() => mockAlbumRepo.getAssetsToHash(album.id)) - .thenAnswer((_) async => [asset]); - when(() => mockStorageRepo.getFileForAsset(asset)) - .thenAnswer((_) async => mockFile); - when(() => mockNativeApi.hashPaths(['image-path'])) - .thenAnswer((_) async => [null]); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset]); + when(() => mockStorageRepo.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); + when(() => mockNativeApi.hashPaths(['image-path'])).thenAnswer((_) async => [null]); when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); await sut.hashAssets(); - final captured = verify(() => mockAssetRepo.updateHashes(captureAny())) - .captured - .first as List; + final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List; expect(captured.length, 0); }); @@ -137,23 +122,17 @@ void main() { when(() => mockFile.length()).thenAnswer((_) async => 1000); when(() => mockFile.path).thenReturn('image-path'); - when(() => mockAlbumRepo.getAll(sortBy: sortBy)) - .thenAnswer((_) async => [album]); - when(() => mockAlbumRepo.getAssetsToHash(album.id)) - .thenAnswer((_) async => [asset]); - when(() => mockStorageRepo.getFileForAsset(asset)) - .thenAnswer((_) async => mockFile); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset]); + when(() => mockStorageRepo.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); final invalidHash = Uint8List.fromList([1, 2, 3]); - when(() => mockNativeApi.hashPaths(['image-path'])) - .thenAnswer((_) async => [invalidHash]); + when(() => mockNativeApi.hashPaths(['image-path'])).thenAnswer((_) async => [invalidHash]); when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); await sut.hashAssets(); - final captured = verify(() => mockAssetRepo.updateHashes(captureAny())) - .captured - .first as List; + final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List; expect(captured.length, 0); }); @@ -176,18 +155,13 @@ void main() { when(() => mockFile2.length()).thenAnswer((_) async => 100); when(() => mockFile2.path).thenReturn('path-2'); - when(() => mockAlbumRepo.getAll(sortBy: sortBy)) - .thenAnswer((_) async => [album]); - when(() => mockAlbumRepo.getAssetsToHash(album.id)) - .thenAnswer((_) async => [asset1, asset2]); - when(() => mockStorageRepo.getFileForAsset(asset1)) - .thenAnswer((_) async => mockFile1); - when(() => mockStorageRepo.getFileForAsset(asset2)) - .thenAnswer((_) async => mockFile2); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset1, asset2]); + when(() => mockStorageRepo.getFileForAsset(asset1.id)).thenAnswer((_) async => mockFile1); + when(() => mockStorageRepo.getFileForAsset(asset2.id)).thenAnswer((_) async => mockFile2); final hash = Uint8List.fromList(List.generate(20, (i) => i)); - when(() => mockNativeApi.hashPaths(any())) - .thenAnswer((_) async => [hash]); + when(() => mockNativeApi.hashPaths(any())).thenAnswer((_) async => [hash]); when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); await sut.hashAssets(); @@ -216,18 +190,13 @@ void main() { when(() => mockFile2.length()).thenAnswer((_) async => 100); when(() => mockFile2.path).thenReturn('path-2'); - when(() => mockAlbumRepo.getAll(sortBy: sortBy)) - .thenAnswer((_) async => [album]); - when(() => mockAlbumRepo.getAssetsToHash(album.id)) - .thenAnswer((_) async => [asset1, asset2]); - when(() => mockStorageRepo.getFileForAsset(asset1)) - .thenAnswer((_) async => mockFile1); - when(() => mockStorageRepo.getFileForAsset(asset2)) - .thenAnswer((_) async => mockFile2); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset1, asset2]); + when(() => mockStorageRepo.getFileForAsset(asset1.id)).thenAnswer((_) async => mockFile1); + when(() => mockStorageRepo.getFileForAsset(asset2.id)).thenAnswer((_) async => mockFile2); final hash = Uint8List.fromList(List.generate(20, (i) => i)); - when(() => mockNativeApi.hashPaths(any())) - .thenAnswer((_) async => [hash]); + when(() => mockNativeApi.hashPaths(any())).thenAnswer((_) async => [hash]); when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); await sut.hashAssets(); @@ -248,25 +217,18 @@ void main() { when(() => mockFile2.length()).thenAnswer((_) async => 100); when(() => mockFile2.path).thenReturn('path-2'); - when(() => mockAlbumRepo.getAll(sortBy: sortBy)) - .thenAnswer((_) async => [album]); - when(() => mockAlbumRepo.getAssetsToHash(album.id)) - .thenAnswer((_) async => [asset1, asset2]); - when(() => mockStorageRepo.getFileForAsset(asset1)) - .thenAnswer((_) async => mockFile1); - when(() => mockStorageRepo.getFileForAsset(asset2)) - .thenAnswer((_) async => mockFile2); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset1, asset2]); + when(() => mockStorageRepo.getFileForAsset(asset1.id)).thenAnswer((_) async => mockFile1); + when(() => mockStorageRepo.getFileForAsset(asset2.id)).thenAnswer((_) async => mockFile2); final validHash = Uint8List.fromList(List.generate(20, (i) => i)); - when(() => mockNativeApi.hashPaths(['path-1', 'path-2'])) - .thenAnswer((_) async => [validHash, null]); + when(() => mockNativeApi.hashPaths(['path-1', 'path-2'])).thenAnswer((_) async => [validHash, null]); when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); await sut.hashAssets(); - final captured = verify(() => mockAssetRepo.updateHashes(captureAny())) - .captured - .first as List; + final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List; expect(captured.length, 1); expect(captured.first.id, asset1.id); }); diff --git a/mobile/test/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart index fd9a86ce4e..ad35a018c8 100644 --- a/mobile/test/domain/services/log_service_test.dart +++ b/mobile/test/domain/services/log_service_test.dart @@ -1,11 +1,11 @@ import 'package:collection/collection.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/interfaces/log.interface.dart'; -import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; @@ -28,8 +28,8 @@ final _kWarnLog = LogMessage( void main() { late LogService sut; - late ILogRepository mockLogRepo; - late IStoreRepository mockStoreRepo; + late IsarLogRepository mockLogRepo; + late IsarStoreRepository mockStoreRepo; setUp(() async { mockLogRepo = MockLogRepository(); @@ -37,10 +37,8 @@ void main() { registerFallbackValue(_kInfoLog); - when(() => mockLogRepo.truncate(limit: any(named: 'limit'))) - .thenAnswer((_) async => {}); - when(() => mockStoreRepo.tryGet(StoreKey.logLevel)) - .thenAnswer((_) async => LogLevel.fine.index); + when(() => mockLogRepo.truncate(limit: any(named: 'limit'))).thenAnswer((_) async => {}); + when(() => mockStoreRepo.tryGet(StoreKey.logLevel)).thenAnswer((_) async => LogLevel.fine.index); when(() => mockLogRepo.getAll()).thenAnswer((_) async => []); when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true); when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true); @@ -57,10 +55,7 @@ void main() { group("Log Service Init:", () { test('Truncates the existing logs on init', () { - final limit = - verify(() => mockLogRepo.truncate(limit: captureAny(named: 'limit'))) - .captured - .firstOrNull as int?; + final limit = verify(() => mockLogRepo.truncate(limit: captureAny(named: 'limit'))).captured.firstOrNull as int?; expect(limit, kLogTruncateLimit); }); @@ -72,8 +67,7 @@ void main() { group("Log Service Set Level:", () { setUp(() async { - when(() => mockStoreRepo.insert(StoreKey.logLevel, any())) - .thenAnswer((_) async => true); + when(() => mockStoreRepo.insert(StoreKey.logLevel, any())).thenAnswer((_) async => true); await sut.setLogLevel(LogLevel.shout); }); @@ -121,7 +115,6 @@ void main() { time.elapse(const Duration(seconds: 6)); final insert = verify(() => mockLogRepo.insertAll(captureAny())); insert.called(1); - // ignore: prefer-correct-json-casts final captured = insert.captured.firstOrNull as List; expect(captured.firstOrNull?.message, _kInfoLog.message); expect(captured.firstOrNull?.logger, _kInfoLog.logger); diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart index 8f4749ac62..c436a05454 100644 --- a/mobile/test/domain/services/store_service_test.dart +++ b/mobile/test/domain/services/store_service_test.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:mocktail/mocktail.dart'; import '../../infrastructure/repository.mock.dart'; @@ -15,7 +15,7 @@ final _kBackupFailedSince = DateTime.utc(2023); void main() { late StoreService sut; - late IStoreRepository mockStoreRepo; + late IsarStoreRepository mockStoreRepo; late StreamController> controller; setUp(() async { @@ -86,8 +86,7 @@ void main() { group('Store Service put:', () { setUp(() { - when(() => mockStoreRepo.insert(any>(), any())) - .thenAnswer((_) async => true); + when(() => mockStoreRepo.insert(any>(), any())).thenAnswer((_) async => true); }); test('Skip insert when value is not modified', () async { @@ -101,8 +100,7 @@ void main() { final newAccessToken = _kAccessToken.toUpperCase(); await sut.put(StoreKey.accessToken, newAccessToken); verify( - () => - mockStoreRepo.insert(StoreKey.accessToken, newAccessToken), + () => mockStoreRepo.insert(StoreKey.accessToken, newAccessToken), ).called(1); expect(sut.tryGet(StoreKey.accessToken), newAccessToken); }); @@ -113,8 +111,7 @@ void main() { setUp(() { valueController = StreamController.broadcast(); - when(() => mockStoreRepo.watch(any>())) - .thenAnswer((_) => valueController.stream); + when(() => mockStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); }); tearDown(() async { @@ -143,14 +140,12 @@ void main() { group('Store Service delete:', () { setUp(() { - when(() => mockStoreRepo.delete(any>())) - .thenAnswer((_) async => true); + when(() => mockStoreRepo.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(() => mockStoreRepo.delete(StoreKey.accessToken)).called(1); }); test('Removes the value from the cache', () async { diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index 3e19306bdb..49ac4467d0 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -1,11 +1,9 @@ -// ignore_for_file: avoid-declaring-call-method, avoid-unnecessary-futures - import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -31,7 +29,7 @@ class _MockCancellationWrapper extends Mock implements _CancellationWrapper {} void main() { late SyncStreamService sut; late SyncStreamRepository mockSyncStreamRepo; - late ISyncApiRepository mockSyncApiRepo; + late SyncApiRepository mockSyncApiRepo; late Function(List, Function()) handleEventsCallback; late _MockAbortCallbackWrapper mockAbortCallbackWrapper; @@ -44,34 +42,59 @@ void main() { when(() => mockAbortCallbackWrapper()).thenReturn(false); - when(() => mockSyncApiRepo.streamChanges(any())) - .thenAnswer((invocation) async { - // ignore: avoid-unsafe-collection-methods + when(() => mockSyncApiRepo.streamChanges(any())).thenAnswer((invocation) async { handleEventsCallback = invocation.positionalArguments.first; }); when(() => mockSyncApiRepo.ack(any())).thenAnswer((_) async => {}); - when(() => mockSyncStreamRepo.updateUsersV1(any())) - .thenAnswer(successHandler); - when(() => mockSyncStreamRepo.deleteUsersV1(any())) - .thenAnswer(successHandler); - when(() => mockSyncStreamRepo.updatePartnerV1(any())) - .thenAnswer(successHandler); - when(() => mockSyncStreamRepo.deletePartnerV1(any())) - .thenAnswer(successHandler); - when(() => mockSyncStreamRepo.updateAssetsV1(any())) - .thenAnswer(successHandler); - when(() => mockSyncStreamRepo.deleteAssetsV1(any())) - .thenAnswer(successHandler); - when(() => mockSyncStreamRepo.updateAssetsExifV1(any())) - .thenAnswer(successHandler); - when(() => mockSyncStreamRepo.updatePartnerAssetsV1(any())) - .thenAnswer(successHandler); - when(() => mockSyncStreamRepo.deletePartnerAssetsV1(any())) - .thenAnswer(successHandler); - when(() => mockSyncStreamRepo.updatePartnerAssetsExifV1(any())) - .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updatePartnerV1(any())).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deletePartnerV1(any())).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateAssetsV1(any())).thenAnswer(successHandler); + when( + () => mockSyncStreamRepo.updateAssetsV1( + any(), + debugLabel: any(named: 'debugLabel'), + ), + ).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteAssetsV1(any())).thenAnswer(successHandler); + when( + () => mockSyncStreamRepo.deleteAssetsV1( + any(), + debugLabel: any(named: 'debugLabel'), + ), + ).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateAssetsExifV1(any())).thenAnswer(successHandler); + when( + () => mockSyncStreamRepo.updateAssetsExifV1( + any(), + debugLabel: any(named: 'debugLabel'), + ), + ).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateMemoriesV1(any())).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteMemoriesV1(any())).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateMemoryAssetsV1(any())).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteMemoryAssetsV1(any())).thenAnswer(successHandler); + when( + () => mockSyncStreamRepo.updateStacksV1( + any(), + debugLabel: any(named: 'debugLabel'), + ), + ).thenAnswer(successHandler); + when( + () => mockSyncStreamRepo.deleteStacksV1( + any(), + debugLabel: any(named: 'debugLabel'), + ), + ).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateUserMetadatasV1(any())).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteUserMetadatasV1(any())).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updatePeopleV1(any())).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deletePeopleV1(any())).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateAssetFacesV1(any())).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteAssetFacesV1(any())).thenAnswer(successHandler); sut = SyncStreamService( syncApiRepository: mockSyncApiRepo, @@ -180,8 +203,7 @@ void main() { final processingCompleter = Completer(); bool handler1Started = false; - when(() => mockSyncStreamRepo.deleteUsersV1(any())) - .thenAnswer((_) async { + when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer((_) async { handler1Started = true; return processingCompleter.future; }); @@ -200,8 +222,7 @@ void main() { SyncStreamStub.partnerDeleteV1, ]; - final processingFuture = - handleEventsCallback(events, mockAbortCallbackWrapper.call); + final processingFuture = handleEventsCallback(events, mockAbortCallbackWrapper.call); await pumpEventQueue(); expect(handler1Started, isTrue); @@ -218,5 +239,92 @@ void main() { verify(() => mockSyncApiRepo.ack(["2"])).called(1); }, ); + + test("processes memory sync events successfully", () async { + final events = [ + SyncStreamStub.memoryV1, + SyncStreamStub.memoryDeleteV1, + SyncStreamStub.memoryToAssetV1, + SyncStreamStub.memoryToAssetDeleteV1, + ]; + + await simulateEvents(events); + + verifyInOrder([ + () => mockSyncStreamRepo.updateMemoriesV1(any()), + () => mockSyncApiRepo.ack(["5"]), + () => mockSyncStreamRepo.deleteMemoriesV1(any()), + () => mockSyncApiRepo.ack(["6"]), + () => mockSyncStreamRepo.updateMemoryAssetsV1(any()), + () => mockSyncApiRepo.ack(["7"]), + () => mockSyncStreamRepo.deleteMemoryAssetsV1(any()), + () => mockSyncApiRepo.ack(["8"]), + ]); + verifyNever(() => mockAbortCallbackWrapper()); + }); + + test("processes mixed memory and user events in correct order", () async { + final events = [ + SyncStreamStub.memoryDeleteV1, + SyncStreamStub.userV1Admin, + SyncStreamStub.memoryToAssetV1, + SyncStreamStub.memoryV1, + ]; + + await simulateEvents(events); + + verifyInOrder([ + () => mockSyncStreamRepo.deleteMemoriesV1(any()), + () => mockSyncApiRepo.ack(["6"]), + () => mockSyncStreamRepo.updateUsersV1(any()), + () => mockSyncApiRepo.ack(["1"]), + () => mockSyncStreamRepo.updateMemoryAssetsV1(any()), + () => mockSyncApiRepo.ack(["7"]), + () => mockSyncStreamRepo.updateMemoriesV1(any()), + () => mockSyncApiRepo.ack(["5"]), + ]); + verifyNever(() => mockAbortCallbackWrapper()); + }); + + test("handles memory sync failure gracefully", () async { + when(() => mockSyncStreamRepo.updateMemoriesV1(any())).thenThrow(Exception("Memory sync failed")); + + final events = [ + SyncStreamStub.memoryV1, + SyncStreamStub.userV1Admin, + ]; + + expect( + () async => await simulateEvents(events), + throwsA(isA()), + ); + }); + + test("processes memory asset events with correct data types", () async { + final events = [SyncStreamStub.memoryToAssetV1]; + + await simulateEvents(events); + + verify(() => mockSyncStreamRepo.updateMemoryAssetsV1(any())).called(1); + verify(() => mockSyncApiRepo.ack(["7"])).called(1); + }); + + test("processes memory delete events with correct data types", () async { + final events = [SyncStreamStub.memoryDeleteV1]; + + await simulateEvents(events); + + verify(() => mockSyncStreamRepo.deleteMemoriesV1(any())).called(1); + verify(() => mockSyncApiRepo.ack(["6"])).called(1); + }); + + test("processes memory create/update events with correct data types", () async { + final events = [SyncStreamStub.memoryV1]; + + await simulateEvents(events); + + verify(() => mockSyncStreamRepo.updateMemoriesV1(any())).called(1); + verify(() => mockSyncApiRepo.ack(["5"])).called(1); + }); }); } diff --git a/mobile/test/domain/services/user_service_test.dart b/mobile/test/domain/services/user_service_test.dart index 5cce565477..b26c243430 100644 --- a/mobile/test/domain/services/user_service_test.dart +++ b/mobile/test/domain/services/user_service_test.dart @@ -29,10 +29,8 @@ void main() { ); registerFallbackValue(UserStub.admin); - when(() => mockStoreService.get(StoreKey.currentUser)) - .thenReturn(UserStub.admin); - when(() => mockStoreService.tryGet(StoreKey.currentUser)) - .thenReturn(UserStub.admin); + when(() => mockStoreService.get(StoreKey.currentUser)).thenReturn(UserStub.admin); + when(() => mockStoreService.tryGet(StoreKey.currentUser)).thenReturn(UserStub.admin); }); group('getMyUser', () { @@ -42,8 +40,7 @@ void main() { }); test('should handle user not found scenario', () { - when(() => mockStoreService.get(StoreKey.currentUser)) - .thenThrow(Exception('User not found')); + when(() => mockStoreService.get(StoreKey.currentUser)).thenThrow(Exception('User not found')); expect(() => sut.getMyUser(), throwsA(isA())); }); @@ -56,8 +53,7 @@ void main() { }); test('should return null if user not found', () { - when(() => mockStoreService.tryGet(StoreKey.currentUser)) - .thenReturn(null); + when(() => mockStoreService.tryGet(StoreKey.currentUser)).thenReturn(null); final result = sut.tryGetMyUser(); expect(result, isNull); }); @@ -65,15 +61,13 @@ void main() { group('watchMyUser', () { test('should return user stream from store', () { - when(() => mockStoreService.watch(StoreKey.currentUser)) - .thenAnswer((_) => Stream.value(UserStub.admin)); + when(() => mockStoreService.watch(StoreKey.currentUser)).thenAnswer((_) => Stream.value(UserStub.admin)); final result = sut.watchMyUser(); expect(result, emits(UserStub.admin)); }); test('should return an empty stream if user not found', () { - when(() => mockStoreService.watch(StoreKey.currentUser)) - .thenAnswer((_) => const Stream.empty()); + when(() => mockStoreService.watch(StoreKey.currentUser)).thenAnswer((_) => const Stream.empty()); final result = sut.watchMyUser(); expect(result, emitsInOrder([])); }); @@ -81,16 +75,12 @@ void main() { group('refreshMyUser', () { 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); + 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(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).called(1); verify(() => mockUserRepo.update(UserStub.admin)).called(1); expect(result, UserStub.admin); }); @@ -110,8 +100,7 @@ void main() { group('createProfileImage', () { test('should return profile image path', () async { const profileImagePath = 'profile.jpg'; - final updatedUser = - UserStub.admin.copyWith(profileImagePath: profileImagePath); + final updatedUser = UserStub.admin.copyWith(profileImagePath: profileImagePath); when( () => mockUserApiRepo.createProfileImage( @@ -119,24 +108,19 @@ void main() { data: Uint8List(0), ), ).thenAnswer((_) async => profileImagePath); - when(() => mockStoreService.put(StoreKey.currentUser, updatedUser)) - .thenAnswer((_) async => true); - when(() => mockUserRepo.update(updatedUser)) - .thenAnswer((_) async => UserStub.admin); + 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)); + final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); - verify(() => mockStoreService.put(StoreKey.currentUser, updatedUser)) - .called(1); + verify(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).called(1); verify(() => mockUserRepo.update(updatedUser)).called(1); expect(result, profileImagePath); }); test('should return null if profile image creation fails', () async { const profileImagePath = 'profile.jpg'; - final updatedUser = - UserStub.admin.copyWith(profileImagePath: profileImagePath); + final updatedUser = UserStub.admin.copyWith(profileImagePath: profileImagePath); when( () => mockUserApiRepo.createProfileImage( @@ -145,8 +129,7 @@ void main() { ), ).thenThrow(Exception('Failed to create profile image')); - final result = - await sut.createProfileImage(profileImagePath, Uint8List(0)); + final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); verifyNever( () => mockStoreService.put(StoreKey.currentUser, updatedUser), ); diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart new file mode 100644 index 0000000000..22131b11bb --- /dev/null +++ b/mobile/test/drift/main/generated/schema.dart @@ -0,0 +1,29 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; +import 'package:drift/internal/migrations.dart'; +import 'schema_v1.dart' as v1; +import 'schema_v2.dart' as v2; +import 'schema_v3.dart' as v3; +import 'schema_v4.dart' as v4; + +class GeneratedHelper implements SchemaInstantiationHelper { + @override + GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { + switch (version) { + case 1: + return v1.DatabaseAtV1(db); + case 2: + return v2.DatabaseAtV2(db); + case 3: + return v3.DatabaseAtV3(db); + case 4: + return v4.DatabaseAtV4(db); + default: + throw MissingSchemaException(version, versions); + } + } + + static const versions = const [1, 2, 3, 4]; +} diff --git a/mobile/test/drift/main/generated/schema_v1.dart b/mobile/test/drift/main/generated/schema_v1.dart new file mode 100644 index 0000000000..75f3bdee4c --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v1.dart @@ -0,0 +1,4657 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isAdmin = GeneratedColumn('is_admin', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn email = + GeneratedColumn('email', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn profileImagePath = GeneratedColumn('profile_image_path', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn('quota_size_in_bytes', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn('quota_usage_in_bytes', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: const CustomExpression('0')); + @override + List get $columns => + [id, name, isAdmin, email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + isAdmin: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_admin'])!, + email: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}email'])!, + profileImagePath: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}profile_image_path']), + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + quotaSizeInBytes: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}quota_size_in_bytes']), + quotaUsageInBytes: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}quota_usage_in_bytes'])!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final bool isAdmin; + final String email; + final String? profileImagePath; + final DateTime updatedAt; + final int? quotaSizeInBytes; + final int quotaUsageInBytes; + const UserEntityData( + {required this.id, + required this.name, + required this.isAdmin, + required this.email, + this.profileImagePath, + required this.updatedAt, + this.quotaSizeInBytes, + required this.quotaUsageInBytes}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['is_admin'] = Variable(isAdmin); + map['email'] = Variable(email); + if (!nullToAbsent || profileImagePath != null) { + map['profile_image_path'] = Variable(profileImagePath); + } + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || quotaSizeInBytes != null) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + } + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + return map; + } + + factory UserEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + isAdmin: serializer.fromJson(json['isAdmin']), + email: serializer.fromJson(json['email']), + profileImagePath: serializer.fromJson(json['profileImagePath']), + updatedAt: serializer.fromJson(json['updatedAt']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'isAdmin': serializer.toJson(isAdmin), + 'email': serializer.toJson(email), + 'profileImagePath': serializer.toJson(profileImagePath), + 'updatedAt': serializer.toJson(updatedAt), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + }; + } + + UserEntityData copyWith( + {String? id, + String? name, + bool? isAdmin, + String? email, + Value profileImagePath = const Value.absent(), + DateTime? updatedAt, + Value quotaSizeInBytes = const Value.absent(), + int? quotaUsageInBytes}) => + UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath.present ? profileImagePath.value : this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes.present ? quotaSizeInBytes.value : this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + email: data.email.present ? data.email.value : this.email, + profileImagePath: data.profileImagePath.present ? data.profileImagePath.value : this.profileImagePath, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + quotaSizeInBytes: data.quotaSizeInBytes.present ? data.quotaSizeInBytes.value : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present ? data.quotaUsageInBytes.value : this.quotaUsageInBytes, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, isAdmin, email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.isAdmin == this.isAdmin && + other.email == this.email && + other.profileImagePath == this.profileImagePath && + other.updatedAt == this.updatedAt && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value isAdmin; + final Value email; + final Value profileImagePath; + final Value updatedAt; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.isAdmin = const Value.absent(), + this.email = const Value.absent(), + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + this.isAdmin = const Value.absent(), + required String email, + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? isAdmin, + Expression? email, + Expression? profileImagePath, + Expression? updatedAt, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (isAdmin != null) 'is_admin': isAdmin, + if (email != null) 'email': email, + if (profileImagePath != null) 'profile_image_path': profileImagePath, + if (updatedAt != null) 'updated_at': updatedAt, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + }); + } + + UserEntityCompanion copyWith( + {Value? id, + Value? name, + Value? isAdmin, + Value? email, + Value? profileImagePath, + Value? updatedAt, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes}) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath ?? this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (profileImagePath.present) { + map['profile_image_path'] = Variable(profileImagePath.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = + GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = + GeneratedColumn('width', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = + GeneratedColumn('height', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn('duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = + GeneratedColumn('checksum', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn('is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn localDateTime = GeneratedColumn('local_date_time', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn thumbHash = + GeneratedColumn('thumb_hash', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn deletedAt = GeneratedColumn('deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn visibility = + GeneratedColumn('visibility', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn stackId = + GeneratedColumn('stack_id', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}checksum'])!, + isFavorite: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + localDateTime: + attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}local_date_time']), + thumbHash: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumb_hash']), + deletedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + livePhotoVideoId: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}live_photo_video_id']), + visibility: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}visibility'])!, + stackId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}stack_id']), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + const RemoteAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + }; + } + + RemoteAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent()}) => + RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present ? durationInSeconds.value : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present ? localDateTime.value : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present ? livePhotoVideoId.value : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present ? data.durationInSeconds.value : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present ? data.localDateTime.value : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present ? data.livePhotoVideoId.value : this.livePhotoVideoId, + visibility: data.visibility.present ? data.visibility.value : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, type, createdAt, updatedAt, width, height, durationInSeconds, id, checksum, + isFavorite, ownerId, localDateTime, thumbHash, deletedAt, livePhotoVideoId, visibility, stackId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId); +} + +class RemoteAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + }); + } + + RemoteAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId}) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = + GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = + GeneratedColumn('width', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = + GeneratedColumn('height', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn('duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = + GeneratedColumn('checksum', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn isFavorite = GeneratedColumn('is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn orientation = GeneratedColumn('orientation', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: const CustomExpression('0')); + @override + List get $columns => + [name, type, createdAt, updatedAt, width, height, durationInSeconds, id, checksum, isFavorite, orientation]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}checksum']), + isFavorite: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + orientation: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}orientation'])!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + const LocalAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + return map; + } + + factory LocalAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + }; + } + + LocalAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation}) => + LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present ? durationInSeconds.value : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + LocalAssetEntityData copyWithCompanion(LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present ? data.durationInSeconds.value : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + orientation: data.orientation.present ? data.orientation.value : this.orientation, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, type, createdAt, updatedAt, width, height, durationInSeconds, id, checksum, isFavorite, orientation); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + }); + } + + LocalAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation}) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn primaryAssetId = GeneratedColumn('primary_asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id)')); + @override + List get $columns => [id, createdAt, updatedAt, ownerId, primaryAssetId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + primaryAssetId: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith( + {String? id, DateTime? createdAt, DateTime? updatedAt, String? ownerId, String? primaryAssetId}) => + StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present ? data.primaryAssetId.value : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId}) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn('user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn key = + GeneratedColumn('key', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn value = + GeneratedColumn('value', aliasedName, false, type: DriftSqlType.blob, requiredDuringInsert: true); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + key: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}key'])!, + value: attachedDatabase.typeMapping.read(DriftSqlType.blob, data['${effectivePrefix}value'])!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({required this.userId, required this.key, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({String? userId, int? key, Uint8List? value}) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({Value? userId, Value? key, Value? value}) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn('shared_by_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn sharedWithId = GeneratedColumn('shared_with_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn inTimeline = GeneratedColumn('in_timeline', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("in_timeline" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}shared_by_id'])!, + sharedWithId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}shared_with_id'])!, + inTimeline: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({required this.sharedById, required this.sharedWithId, required this.inTimeline}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({String? sharedById, String? sharedWithId, bool? inTimeline}) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present ? data.sharedById.value : this.sharedById, + sharedWithId: data.sharedWithId.present ? data.sharedWithId.value : this.sharedWithId, + inTimeline: data.inTimeline.present ? data.inTimeline.value : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({Value? sharedById, Value? sharedWithId, Value? inTimeline}) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn backupSelection = + GeneratedColumn('backup_selection', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn('is_ios_shared_album', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_ios_shared_album" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn marker_ = GeneratedColumn('marker', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))')); + @override + List get $columns => [id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + backupSelection: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}backup_selection'])!, + isIosSharedAlbum: + attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_ios_shared_album'])!, + marker_: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}marker']), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final bool? marker_; + const LocalAlbumEntityData( + {required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.marker_}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith( + {String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value marker_ = const Value.absent()}) => + LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present ? data.backupSelection.value : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present ? data.isIosSharedAlbum.value : this.isIosSharedAlbum, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? marker_}) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES local_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn('album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES local_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass implements Insertable { + final String assetId; + final String albumId; + const LocalAlbumAssetEntityData({required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion(LocalAlbumAssetEntityCompanion data) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && other.assetId == this.assetId && other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({Value? assetId, Value? albumId}) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn city = + GeneratedColumn('city', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn state = + GeneratedColumn('state', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn country = + GeneratedColumn('country', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn dateTimeOriginal = GeneratedColumn( + 'date_time_original', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn description = + GeneratedColumn('description', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn height = + GeneratedColumn('height', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn width = + GeneratedColumn('width', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn exposureTime = GeneratedColumn('exposure_time', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn fNumber = + GeneratedColumn('f_number', aliasedName, true, type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn fileSize = + GeneratedColumn('file_size', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn focalLength = GeneratedColumn('focal_length', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn latitude = + GeneratedColumn('latitude', aliasedName, true, type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn longitude = + GeneratedColumn('longitude', aliasedName, true, type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn iso = + GeneratedColumn('iso', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn make = + GeneratedColumn('make', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn model = + GeneratedColumn('model', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn lens = + GeneratedColumn('lens', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn orientation = + GeneratedColumn('orientation', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn timeZone = + GeneratedColumn('time_zone', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn rating = + GeneratedColumn('rating', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn projectionType = GeneratedColumn('projection_type', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + city: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}city']), + state: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}state']), + country: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}country']), + dateTimeOriginal: + attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}date_time_original']), + description: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}description']), + height: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}height']), + width: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}width']), + exposureTime: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}exposure_time']), + fNumber: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}f_number']), + fileSize: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}file_size']), + focalLength: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}focal_length']), + latitude: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}latitude']), + longitude: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}longitude']), + iso: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}iso']), + make: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}make']), + model: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}model']), + lens: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}lens']), + orientation: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}orientation']), + timeZone: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}time_zone']), + rating: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}rating']), + projectionType: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}projection_type']), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData( + {required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson(json['dateTimeOriginal']), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith( + {String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent()}) => + RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present ? dateTimeOriginal.value : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present ? projectionType.value : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present ? data.dateTimeOriginal.value : this.dateTimeOriginal, + description: data.description.present ? data.description.value : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present ? data.exposureTime.value : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present ? data.focalLength.value : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present ? data.orientation.value : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present ? data.projectionType.value : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith( + {Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType}) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn('description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: false, defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn('thumbnail_asset_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE SET NULL')); + late final GeneratedColumn isActivityEnabled = GeneratedColumn('is_activity_enabled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_activity_enabled" IN (0, 1))'), + defaultValue: const CustomExpression('1')); + late final GeneratedColumn order = + GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => + [id, name, description, createdAt, updatedAt, ownerId, thumbnailAssetId, isActivityEnabled, order]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}description'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + thumbnailAssetId: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumbnail_asset_id']), + isActivityEnabled: + attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_activity_enabled'])!, + order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData( + {required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith( + {String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order}) => + RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present ? thumbnailAssetId.value : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present ? data.description.value : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present ? data.thumbnailAssetId.value : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present ? data.isActivityEnabled.value : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, description, createdAt, updatedAt, ownerId, thumbnailAssetId, isActivityEnabled, order); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order}) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn('album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion(RemoteAlbumAssetEntityCompanion data) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && other.assetId == this.assetId && other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({Value? assetId, Value? albumId}) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn('album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn userId = GeneratedColumn('user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn role = + GeneratedColumn('role', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + userId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + role: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}role'])!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({required this.albumId, required this.userId, required this.role}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({String? albumId, String? userId, int? role}) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion(RemoteAlbumUserEntityCompanion data) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({Value? albumId, Value? userId, Value? role}) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn('deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn type = + GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn data = + GeneratedColumn('data', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isSaved = GeneratedColumn('is_saved', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_saved" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn memoryAt = GeneratedColumn('memory_at', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn seenAt = + GeneratedColumn('seen_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn showAt = + GeneratedColumn('show_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn hideAt = + GeneratedColumn('hide_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => + [id, createdAt, updatedAt, deletedAt, ownerId, type, data, isSaved, memoryAt, seenAt, showAt, hideAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + deletedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, + data: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}data'])!, + isSaved: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_saved'])!, + memoryAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}memory_at'])!, + seenAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}seen_at']), + showAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}show_at']), + hideAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}hide_at']), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent()}) => + MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, deletedAt, ownerId, type, data, isSaved, memoryAt, seenAt, showAt, hideAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt}) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn memoryId = GeneratedColumn('memory_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES memory_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + memoryId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}memory_id'])!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && other.assetId == this.assetId && other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({Value? assetId, Value? memoryId}) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn faceAssetId = GeneratedColumn('face_asset_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn thumbnailPath = GeneratedColumn('thumbnail_path', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn('is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))')); + late final GeneratedColumn isHidden = GeneratedColumn('is_hidden', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_hidden" IN (0, 1))')); + late final GeneratedColumn color = + GeneratedColumn('color', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn birthDate = GeneratedColumn('birth_date', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => + [id, createdAt, updatedAt, ownerId, name, faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + faceAssetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}face_asset_id']), + thumbnailPath: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumbnail_path'])!, + isFavorite: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + isHidden: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_hidden'])!, + color: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}color']), + birthDate: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}birth_date']), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final String thumbnailPath; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.thumbnailPath, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['thumbnail_path'] = Variable(thumbnailPath); + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + thumbnailPath: serializer.fromJson(json['thumbnailPath']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'thumbnailPath': serializer.toJson(thumbnailPath), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + String? thumbnailPath, + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent()}) => + PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present ? data.faceAssetId.value : this.faceAssetId, + thumbnailPath: data.thumbnailPath.present ? data.thumbnailPath.value : this.thumbnailPath, + isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, createdAt, updatedAt, ownerId, name, faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.thumbnailPath == this.thumbnailPath && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value thumbnailPath; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.thumbnailPath = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required String thumbnailPath, + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + thumbnailPath = Value(thumbnailPath), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? thumbnailPath, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (thumbnailPath != null) 'thumbnail_path': thumbnailPath, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? thumbnailPath, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate}) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (thumbnailPath.present) { + map['thumbnail_path'] = Variable(thumbnailPath.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV1 extends GeneratedDatabase { + DatabaseAtV1(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final Index idxLocalAssetChecksum = + Index('idx_local_asset_checksum', 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + late final Index uQRemoteAssetOwnerChecksum = Index('UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + late final Index idxRemoteAssetChecksum = + Index('idx_remote_asset_checksum', 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = LocalAlbumAssetEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = RemoteAlbumUserEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + @override + Iterable> get allTables => allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + localAssetEntity, + stackEntity, + idxLocalAssetChecksum, + uQRemoteAssetOwnerChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAlbumAssetEntity, + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity + ]; + @override + int get schemaVersion => 1; + @override + DriftDatabaseOptions get options => const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v2.dart b/mobile/test/drift/main/generated/schema_v2.dart new file mode 100644 index 0000000000..3d705e0454 --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v2.dart @@ -0,0 +1,4657 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isAdmin = GeneratedColumn('is_admin', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn email = + GeneratedColumn('email', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn profileImagePath = GeneratedColumn('profile_image_path', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn('quota_size_in_bytes', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn('quota_usage_in_bytes', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: const CustomExpression('0')); + @override + List get $columns => + [id, name, isAdmin, email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + isAdmin: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_admin'])!, + email: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}email'])!, + profileImagePath: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}profile_image_path']), + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + quotaSizeInBytes: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}quota_size_in_bytes']), + quotaUsageInBytes: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}quota_usage_in_bytes'])!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final bool isAdmin; + final String email; + final String? profileImagePath; + final DateTime updatedAt; + final int? quotaSizeInBytes; + final int quotaUsageInBytes; + const UserEntityData( + {required this.id, + required this.name, + required this.isAdmin, + required this.email, + this.profileImagePath, + required this.updatedAt, + this.quotaSizeInBytes, + required this.quotaUsageInBytes}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['is_admin'] = Variable(isAdmin); + map['email'] = Variable(email); + if (!nullToAbsent || profileImagePath != null) { + map['profile_image_path'] = Variable(profileImagePath); + } + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || quotaSizeInBytes != null) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + } + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + return map; + } + + factory UserEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + isAdmin: serializer.fromJson(json['isAdmin']), + email: serializer.fromJson(json['email']), + profileImagePath: serializer.fromJson(json['profileImagePath']), + updatedAt: serializer.fromJson(json['updatedAt']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'isAdmin': serializer.toJson(isAdmin), + 'email': serializer.toJson(email), + 'profileImagePath': serializer.toJson(profileImagePath), + 'updatedAt': serializer.toJson(updatedAt), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + }; + } + + UserEntityData copyWith( + {String? id, + String? name, + bool? isAdmin, + String? email, + Value profileImagePath = const Value.absent(), + DateTime? updatedAt, + Value quotaSizeInBytes = const Value.absent(), + int? quotaUsageInBytes}) => + UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath.present ? profileImagePath.value : this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes.present ? quotaSizeInBytes.value : this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + email: data.email.present ? data.email.value : this.email, + profileImagePath: data.profileImagePath.present ? data.profileImagePath.value : this.profileImagePath, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + quotaSizeInBytes: data.quotaSizeInBytes.present ? data.quotaSizeInBytes.value : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present ? data.quotaUsageInBytes.value : this.quotaUsageInBytes, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, isAdmin, email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.isAdmin == this.isAdmin && + other.email == this.email && + other.profileImagePath == this.profileImagePath && + other.updatedAt == this.updatedAt && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value isAdmin; + final Value email; + final Value profileImagePath; + final Value updatedAt; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.isAdmin = const Value.absent(), + this.email = const Value.absent(), + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + this.isAdmin = const Value.absent(), + required String email, + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? isAdmin, + Expression? email, + Expression? profileImagePath, + Expression? updatedAt, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (isAdmin != null) 'is_admin': isAdmin, + if (email != null) 'email': email, + if (profileImagePath != null) 'profile_image_path': profileImagePath, + if (updatedAt != null) 'updated_at': updatedAt, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + }); + } + + UserEntityCompanion copyWith( + {Value? id, + Value? name, + Value? isAdmin, + Value? email, + Value? profileImagePath, + Value? updatedAt, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes}) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath ?? this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (profileImagePath.present) { + map['profile_image_path'] = Variable(profileImagePath.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = + GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = + GeneratedColumn('width', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = + GeneratedColumn('height', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn('duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = + GeneratedColumn('checksum', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn('is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn localDateTime = GeneratedColumn('local_date_time', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn thumbHash = + GeneratedColumn('thumb_hash', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn deletedAt = GeneratedColumn('deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn visibility = + GeneratedColumn('visibility', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn stackId = + GeneratedColumn('stack_id', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}checksum'])!, + isFavorite: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + localDateTime: + attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}local_date_time']), + thumbHash: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumb_hash']), + deletedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + livePhotoVideoId: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}live_photo_video_id']), + visibility: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}visibility'])!, + stackId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}stack_id']), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + const RemoteAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + }; + } + + RemoteAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent()}) => + RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present ? durationInSeconds.value : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present ? localDateTime.value : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present ? livePhotoVideoId.value : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present ? data.durationInSeconds.value : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present ? data.localDateTime.value : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present ? data.livePhotoVideoId.value : this.livePhotoVideoId, + visibility: data.visibility.present ? data.visibility.value : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, type, createdAt, updatedAt, width, height, durationInSeconds, id, checksum, + isFavorite, ownerId, localDateTime, thumbHash, deletedAt, livePhotoVideoId, visibility, stackId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId); +} + +class RemoteAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + }); + } + + RemoteAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId}) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = + GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = + GeneratedColumn('width', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = + GeneratedColumn('height', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn('duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = + GeneratedColumn('checksum', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn isFavorite = GeneratedColumn('is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn orientation = GeneratedColumn('orientation', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: const CustomExpression('0')); + @override + List get $columns => + [name, type, createdAt, updatedAt, width, height, durationInSeconds, id, checksum, isFavorite, orientation]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}checksum']), + isFavorite: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + orientation: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}orientation'])!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + const LocalAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + return map; + } + + factory LocalAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + }; + } + + LocalAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation}) => + LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present ? durationInSeconds.value : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + LocalAssetEntityData copyWithCompanion(LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present ? data.durationInSeconds.value : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + orientation: data.orientation.present ? data.orientation.value : this.orientation, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, type, createdAt, updatedAt, width, height, durationInSeconds, id, checksum, isFavorite, orientation); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + }); + } + + LocalAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation}) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn primaryAssetId = GeneratedColumn('primary_asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id)')); + @override + List get $columns => [id, createdAt, updatedAt, ownerId, primaryAssetId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + primaryAssetId: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith( + {String? id, DateTime? createdAt, DateTime? updatedAt, String? ownerId, String? primaryAssetId}) => + StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present ? data.primaryAssetId.value : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId}) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn('user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn key = + GeneratedColumn('key', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn value = + GeneratedColumn('value', aliasedName, false, type: DriftSqlType.blob, requiredDuringInsert: true); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + key: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}key'])!, + value: attachedDatabase.typeMapping.read(DriftSqlType.blob, data['${effectivePrefix}value'])!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({required this.userId, required this.key, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({String? userId, int? key, Uint8List? value}) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({Value? userId, Value? key, Value? value}) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn('shared_by_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn sharedWithId = GeneratedColumn('shared_with_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn inTimeline = GeneratedColumn('in_timeline', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("in_timeline" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}shared_by_id'])!, + sharedWithId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}shared_with_id'])!, + inTimeline: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({required this.sharedById, required this.sharedWithId, required this.inTimeline}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({String? sharedById, String? sharedWithId, bool? inTimeline}) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present ? data.sharedById.value : this.sharedById, + sharedWithId: data.sharedWithId.present ? data.sharedWithId.value : this.sharedWithId, + inTimeline: data.inTimeline.present ? data.inTimeline.value : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({Value? sharedById, Value? sharedWithId, Value? inTimeline}) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn backupSelection = + GeneratedColumn('backup_selection', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn('is_ios_shared_album', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_ios_shared_album" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn marker_ = GeneratedColumn('marker', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))')); + @override + List get $columns => [id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + backupSelection: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}backup_selection'])!, + isIosSharedAlbum: + attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_ios_shared_album'])!, + marker_: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}marker']), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final bool? marker_; + const LocalAlbumEntityData( + {required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.marker_}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith( + {String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value marker_ = const Value.absent()}) => + LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present ? data.backupSelection.value : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present ? data.isIosSharedAlbum.value : this.isIosSharedAlbum, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? marker_}) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES local_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn('album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES local_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass implements Insertable { + final String assetId; + final String albumId; + const LocalAlbumAssetEntityData({required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion(LocalAlbumAssetEntityCompanion data) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && other.assetId == this.assetId && other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({Value? assetId, Value? albumId}) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn city = + GeneratedColumn('city', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn state = + GeneratedColumn('state', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn country = + GeneratedColumn('country', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn dateTimeOriginal = GeneratedColumn( + 'date_time_original', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn description = + GeneratedColumn('description', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn height = + GeneratedColumn('height', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn width = + GeneratedColumn('width', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn exposureTime = GeneratedColumn('exposure_time', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn fNumber = + GeneratedColumn('f_number', aliasedName, true, type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn fileSize = + GeneratedColumn('file_size', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn focalLength = GeneratedColumn('focal_length', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn latitude = + GeneratedColumn('latitude', aliasedName, true, type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn longitude = + GeneratedColumn('longitude', aliasedName, true, type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn iso = + GeneratedColumn('iso', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn make = + GeneratedColumn('make', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn model = + GeneratedColumn('model', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn lens = + GeneratedColumn('lens', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn orientation = + GeneratedColumn('orientation', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn timeZone = + GeneratedColumn('time_zone', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn rating = + GeneratedColumn('rating', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn projectionType = GeneratedColumn('projection_type', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + city: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}city']), + state: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}state']), + country: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}country']), + dateTimeOriginal: + attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}date_time_original']), + description: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}description']), + height: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}height']), + width: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}width']), + exposureTime: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}exposure_time']), + fNumber: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}f_number']), + fileSize: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}file_size']), + focalLength: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}focal_length']), + latitude: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}latitude']), + longitude: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}longitude']), + iso: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}iso']), + make: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}make']), + model: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}model']), + lens: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}lens']), + orientation: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}orientation']), + timeZone: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}time_zone']), + rating: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}rating']), + projectionType: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}projection_type']), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData( + {required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson(json['dateTimeOriginal']), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith( + {String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent()}) => + RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present ? dateTimeOriginal.value : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present ? projectionType.value : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present ? data.dateTimeOriginal.value : this.dateTimeOriginal, + description: data.description.present ? data.description.value : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present ? data.exposureTime.value : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present ? data.focalLength.value : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present ? data.orientation.value : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present ? data.projectionType.value : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith( + {Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType}) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn('description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: false, defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn('thumbnail_asset_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE SET NULL')); + late final GeneratedColumn isActivityEnabled = GeneratedColumn('is_activity_enabled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_activity_enabled" IN (0, 1))'), + defaultValue: const CustomExpression('1')); + late final GeneratedColumn order = + GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => + [id, name, description, createdAt, updatedAt, ownerId, thumbnailAssetId, isActivityEnabled, order]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}description'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + thumbnailAssetId: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumbnail_asset_id']), + isActivityEnabled: + attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_activity_enabled'])!, + order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData( + {required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith( + {String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order}) => + RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present ? thumbnailAssetId.value : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present ? data.description.value : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present ? data.thumbnailAssetId.value : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present ? data.isActivityEnabled.value : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, description, createdAt, updatedAt, ownerId, thumbnailAssetId, isActivityEnabled, order); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order}) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn('album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion(RemoteAlbumAssetEntityCompanion data) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && other.assetId == this.assetId && other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({Value? assetId, Value? albumId}) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn('album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn userId = GeneratedColumn('user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn role = + GeneratedColumn('role', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + userId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + role: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}role'])!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({required this.albumId, required this.userId, required this.role}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({String? albumId, String? userId, int? role}) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion(RemoteAlbumUserEntityCompanion data) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({Value? albumId, Value? userId, Value? role}) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn('deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn type = + GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn data = + GeneratedColumn('data', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isSaved = GeneratedColumn('is_saved', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_saved" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn memoryAt = GeneratedColumn('memory_at', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn seenAt = + GeneratedColumn('seen_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn showAt = + GeneratedColumn('show_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn hideAt = + GeneratedColumn('hide_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => + [id, createdAt, updatedAt, deletedAt, ownerId, type, data, isSaved, memoryAt, seenAt, showAt, hideAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + deletedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, + data: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}data'])!, + isSaved: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_saved'])!, + memoryAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}memory_at'])!, + seenAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}seen_at']), + showAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}show_at']), + hideAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}hide_at']), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent()}) => + MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, deletedAt, ownerId, type, data, isSaved, memoryAt, seenAt, showAt, hideAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt}) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn memoryId = GeneratedColumn('memory_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES memory_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + memoryId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}memory_id'])!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && other.assetId == this.assetId && other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({Value? assetId, Value? memoryId}) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn faceAssetId = GeneratedColumn('face_asset_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn thumbnailPath = GeneratedColumn('thumbnail_path', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn('is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))')); + late final GeneratedColumn isHidden = GeneratedColumn('is_hidden', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_hidden" IN (0, 1))')); + late final GeneratedColumn color = + GeneratedColumn('color', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn birthDate = GeneratedColumn('birth_date', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => + [id, createdAt, updatedAt, ownerId, name, faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + faceAssetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}face_asset_id']), + thumbnailPath: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumbnail_path'])!, + isFavorite: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + isHidden: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_hidden'])!, + color: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}color']), + birthDate: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}birth_date']), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final String thumbnailPath; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.thumbnailPath, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['thumbnail_path'] = Variable(thumbnailPath); + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + thumbnailPath: serializer.fromJson(json['thumbnailPath']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'thumbnailPath': serializer.toJson(thumbnailPath), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + String? thumbnailPath, + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent()}) => + PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present ? data.faceAssetId.value : this.faceAssetId, + thumbnailPath: data.thumbnailPath.present ? data.thumbnailPath.value : this.thumbnailPath, + isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, createdAt, updatedAt, ownerId, name, faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.thumbnailPath == this.thumbnailPath && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value thumbnailPath; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.thumbnailPath = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required String thumbnailPath, + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + thumbnailPath = Value(thumbnailPath), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? thumbnailPath, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (thumbnailPath != null) 'thumbnail_path': thumbnailPath, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? thumbnailPath, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate}) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (thumbnailPath.present) { + map['thumbnail_path'] = Variable(thumbnailPath.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV2 extends GeneratedDatabase { + DatabaseAtV2(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final Index idxLocalAssetChecksum = + Index('idx_local_asset_checksum', 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + late final Index uQRemoteAssetOwnerChecksum = Index('UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + late final Index idxRemoteAssetChecksum = + Index('idx_remote_asset_checksum', 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = LocalAlbumAssetEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = RemoteAlbumUserEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + @override + Iterable> get allTables => allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + localAssetEntity, + stackEntity, + idxLocalAssetChecksum, + uQRemoteAssetOwnerChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAlbumAssetEntity, + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity + ]; + @override + int get schemaVersion => 2; + @override + DriftDatabaseOptions get options => const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v3.dart b/mobile/test/drift/main/generated/schema_v3.dart new file mode 100644 index 0000000000..711cf85253 --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v3.dart @@ -0,0 +1,4655 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isAdmin = GeneratedColumn('is_admin', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn email = + GeneratedColumn('email', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn profileImagePath = GeneratedColumn('profile_image_path', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn('quota_size_in_bytes', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn('quota_usage_in_bytes', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: const CustomExpression('0')); + @override + List get $columns => + [id, name, isAdmin, email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + isAdmin: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_admin'])!, + email: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}email'])!, + profileImagePath: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}profile_image_path']), + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + quotaSizeInBytes: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}quota_size_in_bytes']), + quotaUsageInBytes: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}quota_usage_in_bytes'])!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final bool isAdmin; + final String email; + final String? profileImagePath; + final DateTime updatedAt; + final int? quotaSizeInBytes; + final int quotaUsageInBytes; + const UserEntityData( + {required this.id, + required this.name, + required this.isAdmin, + required this.email, + this.profileImagePath, + required this.updatedAt, + this.quotaSizeInBytes, + required this.quotaUsageInBytes}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['is_admin'] = Variable(isAdmin); + map['email'] = Variable(email); + if (!nullToAbsent || profileImagePath != null) { + map['profile_image_path'] = Variable(profileImagePath); + } + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || quotaSizeInBytes != null) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + } + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + return map; + } + + factory UserEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + isAdmin: serializer.fromJson(json['isAdmin']), + email: serializer.fromJson(json['email']), + profileImagePath: serializer.fromJson(json['profileImagePath']), + updatedAt: serializer.fromJson(json['updatedAt']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'isAdmin': serializer.toJson(isAdmin), + 'email': serializer.toJson(email), + 'profileImagePath': serializer.toJson(profileImagePath), + 'updatedAt': serializer.toJson(updatedAt), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + }; + } + + UserEntityData copyWith( + {String? id, + String? name, + bool? isAdmin, + String? email, + Value profileImagePath = const Value.absent(), + DateTime? updatedAt, + Value quotaSizeInBytes = const Value.absent(), + int? quotaUsageInBytes}) => + UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath.present ? profileImagePath.value : this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes.present ? quotaSizeInBytes.value : this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + email: data.email.present ? data.email.value : this.email, + profileImagePath: data.profileImagePath.present ? data.profileImagePath.value : this.profileImagePath, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + quotaSizeInBytes: data.quotaSizeInBytes.present ? data.quotaSizeInBytes.value : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present ? data.quotaUsageInBytes.value : this.quotaUsageInBytes, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, isAdmin, email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.isAdmin == this.isAdmin && + other.email == this.email && + other.profileImagePath == this.profileImagePath && + other.updatedAt == this.updatedAt && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value isAdmin; + final Value email; + final Value profileImagePath; + final Value updatedAt; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.isAdmin = const Value.absent(), + this.email = const Value.absent(), + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + this.isAdmin = const Value.absent(), + required String email, + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? isAdmin, + Expression? email, + Expression? profileImagePath, + Expression? updatedAt, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (isAdmin != null) 'is_admin': isAdmin, + if (email != null) 'email': email, + if (profileImagePath != null) 'profile_image_path': profileImagePath, + if (updatedAt != null) 'updated_at': updatedAt, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + }); + } + + UserEntityCompanion copyWith( + {Value? id, + Value? name, + Value? isAdmin, + Value? email, + Value? profileImagePath, + Value? updatedAt, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes}) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath ?? this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (profileImagePath.present) { + map['profile_image_path'] = Variable(profileImagePath.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = + GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = + GeneratedColumn('width', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = + GeneratedColumn('height', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn('duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = + GeneratedColumn('checksum', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn('is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn localDateTime = GeneratedColumn('local_date_time', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn thumbHash = + GeneratedColumn('thumb_hash', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn deletedAt = GeneratedColumn('deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn visibility = + GeneratedColumn('visibility', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn stackId = + GeneratedColumn('stack_id', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}checksum'])!, + isFavorite: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + localDateTime: + attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}local_date_time']), + thumbHash: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumb_hash']), + deletedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + livePhotoVideoId: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}live_photo_video_id']), + visibility: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}visibility'])!, + stackId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}stack_id']), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + const RemoteAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + }; + } + + RemoteAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent()}) => + RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present ? durationInSeconds.value : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present ? localDateTime.value : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present ? livePhotoVideoId.value : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present ? data.durationInSeconds.value : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present ? data.localDateTime.value : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present ? data.livePhotoVideoId.value : this.livePhotoVideoId, + visibility: data.visibility.present ? data.visibility.value : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, type, createdAt, updatedAt, width, height, durationInSeconds, id, checksum, + isFavorite, ownerId, localDateTime, thumbHash, deletedAt, livePhotoVideoId, visibility, stackId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId); +} + +class RemoteAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + }); + } + + RemoteAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId}) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = + GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = + GeneratedColumn('width', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = + GeneratedColumn('height', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn('duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = + GeneratedColumn('checksum', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn isFavorite = GeneratedColumn('is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn orientation = GeneratedColumn('orientation', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: const CustomExpression('0')); + @override + List get $columns => + [name, type, createdAt, updatedAt, width, height, durationInSeconds, id, checksum, isFavorite, orientation]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}checksum']), + isFavorite: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + orientation: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}orientation'])!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + const LocalAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + return map; + } + + factory LocalAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + }; + } + + LocalAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation}) => + LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present ? durationInSeconds.value : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + LocalAssetEntityData copyWithCompanion(LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present ? data.durationInSeconds.value : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + orientation: data.orientation.present ? data.orientation.value : this.orientation, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, type, createdAt, updatedAt, width, height, durationInSeconds, id, checksum, isFavorite, orientation); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + }); + } + + LocalAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation}) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn primaryAssetId = GeneratedColumn('primary_asset_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, updatedAt, ownerId, primaryAssetId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + primaryAssetId: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith( + {String? id, DateTime? createdAt, DateTime? updatedAt, String? ownerId, String? primaryAssetId}) => + StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present ? data.primaryAssetId.value : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId}) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn('user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn key = + GeneratedColumn('key', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn value = + GeneratedColumn('value', aliasedName, false, type: DriftSqlType.blob, requiredDuringInsert: true); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + key: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}key'])!, + value: attachedDatabase.typeMapping.read(DriftSqlType.blob, data['${effectivePrefix}value'])!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({required this.userId, required this.key, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({String? userId, int? key, Uint8List? value}) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({Value? userId, Value? key, Value? value}) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn('shared_by_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn sharedWithId = GeneratedColumn('shared_with_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn inTimeline = GeneratedColumn('in_timeline', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("in_timeline" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}shared_by_id'])!, + sharedWithId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}shared_with_id'])!, + inTimeline: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({required this.sharedById, required this.sharedWithId, required this.inTimeline}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({String? sharedById, String? sharedWithId, bool? inTimeline}) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present ? data.sharedById.value : this.sharedById, + sharedWithId: data.sharedWithId.present ? data.sharedWithId.value : this.sharedWithId, + inTimeline: data.inTimeline.present ? data.inTimeline.value : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({Value? sharedById, Value? sharedWithId, Value? inTimeline}) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn backupSelection = + GeneratedColumn('backup_selection', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn('is_ios_shared_album', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_ios_shared_album" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn marker_ = GeneratedColumn('marker', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))')); + @override + List get $columns => [id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + backupSelection: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}backup_selection'])!, + isIosSharedAlbum: + attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_ios_shared_album'])!, + marker_: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}marker']), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final bool? marker_; + const LocalAlbumEntityData( + {required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.marker_}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith( + {String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value marker_ = const Value.absent()}) => + LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present ? data.backupSelection.value : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present ? data.isIosSharedAlbum.value : this.isIosSharedAlbum, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? marker_}) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES local_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn('album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES local_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass implements Insertable { + final String assetId; + final String albumId; + const LocalAlbumAssetEntityData({required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion(LocalAlbumAssetEntityCompanion data) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && other.assetId == this.assetId && other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({Value? assetId, Value? albumId}) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn city = + GeneratedColumn('city', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn state = + GeneratedColumn('state', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn country = + GeneratedColumn('country', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn dateTimeOriginal = GeneratedColumn( + 'date_time_original', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn description = + GeneratedColumn('description', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn height = + GeneratedColumn('height', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn width = + GeneratedColumn('width', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn exposureTime = GeneratedColumn('exposure_time', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn fNumber = + GeneratedColumn('f_number', aliasedName, true, type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn fileSize = + GeneratedColumn('file_size', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn focalLength = GeneratedColumn('focal_length', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn latitude = + GeneratedColumn('latitude', aliasedName, true, type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn longitude = + GeneratedColumn('longitude', aliasedName, true, type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn iso = + GeneratedColumn('iso', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn make = + GeneratedColumn('make', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn model = + GeneratedColumn('model', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn lens = + GeneratedColumn('lens', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn orientation = + GeneratedColumn('orientation', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn timeZone = + GeneratedColumn('time_zone', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn rating = + GeneratedColumn('rating', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn projectionType = GeneratedColumn('projection_type', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + city: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}city']), + state: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}state']), + country: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}country']), + dateTimeOriginal: + attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}date_time_original']), + description: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}description']), + height: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}height']), + width: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}width']), + exposureTime: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}exposure_time']), + fNumber: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}f_number']), + fileSize: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}file_size']), + focalLength: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}focal_length']), + latitude: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}latitude']), + longitude: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}longitude']), + iso: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}iso']), + make: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}make']), + model: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}model']), + lens: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}lens']), + orientation: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}orientation']), + timeZone: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}time_zone']), + rating: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}rating']), + projectionType: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}projection_type']), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData( + {required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson(json['dateTimeOriginal']), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith( + {String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent()}) => + RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present ? dateTimeOriginal.value : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present ? projectionType.value : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present ? data.dateTimeOriginal.value : this.dateTimeOriginal, + description: data.description.present ? data.description.value : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present ? data.exposureTime.value : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present ? data.focalLength.value : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present ? data.orientation.value : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present ? data.projectionType.value : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith( + {Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType}) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn('description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: false, defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn('thumbnail_asset_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE SET NULL')); + late final GeneratedColumn isActivityEnabled = GeneratedColumn('is_activity_enabled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_activity_enabled" IN (0, 1))'), + defaultValue: const CustomExpression('1')); + late final GeneratedColumn order = + GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => + [id, name, description, createdAt, updatedAt, ownerId, thumbnailAssetId, isActivityEnabled, order]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}description'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + thumbnailAssetId: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumbnail_asset_id']), + isActivityEnabled: + attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_activity_enabled'])!, + order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData( + {required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith( + {String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order}) => + RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present ? thumbnailAssetId.value : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present ? data.description.value : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present ? data.thumbnailAssetId.value : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present ? data.isActivityEnabled.value : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, description, createdAt, updatedAt, ownerId, thumbnailAssetId, isActivityEnabled, order); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order}) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn('album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion(RemoteAlbumAssetEntityCompanion data) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && other.assetId == this.assetId && other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({Value? assetId, Value? albumId}) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn('album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn userId = GeneratedColumn('user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn role = + GeneratedColumn('role', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + userId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + role: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}role'])!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({required this.albumId, required this.userId, required this.role}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({String? albumId, String? userId, int? role}) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion(RemoteAlbumUserEntityCompanion data) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({Value? albumId, Value? userId, Value? role}) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn('deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn type = + GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn data = + GeneratedColumn('data', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isSaved = GeneratedColumn('is_saved', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_saved" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn memoryAt = GeneratedColumn('memory_at', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn seenAt = + GeneratedColumn('seen_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn showAt = + GeneratedColumn('show_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn hideAt = + GeneratedColumn('hide_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => + [id, createdAt, updatedAt, deletedAt, ownerId, type, data, isSaved, memoryAt, seenAt, showAt, hideAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + deletedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, + data: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}data'])!, + isSaved: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_saved'])!, + memoryAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}memory_at'])!, + seenAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}seen_at']), + showAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}show_at']), + hideAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}hide_at']), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent()}) => + MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, deletedAt, ownerId, type, data, isSaved, memoryAt, seenAt, showAt, hideAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt}) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn memoryId = GeneratedColumn('memory_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES memory_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + memoryId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}memory_id'])!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && other.assetId == this.assetId && other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({Value? assetId, Value? memoryId}) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn faceAssetId = GeneratedColumn('face_asset_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn thumbnailPath = GeneratedColumn('thumbnail_path', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn('is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))')); + late final GeneratedColumn isHidden = GeneratedColumn('is_hidden', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_hidden" IN (0, 1))')); + late final GeneratedColumn color = + GeneratedColumn('color', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn birthDate = GeneratedColumn('birth_date', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => + [id, createdAt, updatedAt, ownerId, name, faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + faceAssetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}face_asset_id']), + thumbnailPath: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumbnail_path'])!, + isFavorite: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + isHidden: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_hidden'])!, + color: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}color']), + birthDate: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}birth_date']), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final String thumbnailPath; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.thumbnailPath, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['thumbnail_path'] = Variable(thumbnailPath); + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + thumbnailPath: serializer.fromJson(json['thumbnailPath']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'thumbnailPath': serializer.toJson(thumbnailPath), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + String? thumbnailPath, + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent()}) => + PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present ? data.faceAssetId.value : this.faceAssetId, + thumbnailPath: data.thumbnailPath.present ? data.thumbnailPath.value : this.thumbnailPath, + isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, createdAt, updatedAt, ownerId, name, faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.thumbnailPath == this.thumbnailPath && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value thumbnailPath; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.thumbnailPath = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required String thumbnailPath, + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + thumbnailPath = Value(thumbnailPath), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? thumbnailPath, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (thumbnailPath != null) 'thumbnail_path': thumbnailPath, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? thumbnailPath, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate}) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (thumbnailPath.present) { + map['thumbnail_path'] = Variable(thumbnailPath.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV3 extends GeneratedDatabase { + DatabaseAtV3(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final Index idxLocalAssetChecksum = + Index('idx_local_asset_checksum', 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + late final Index uQRemoteAssetOwnerChecksum = Index('UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + late final Index idxRemoteAssetChecksum = + Index('idx_remote_asset_checksum', 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = LocalAlbumAssetEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = RemoteAlbumUserEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + @override + Iterable> get allTables => allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + localAssetEntity, + stackEntity, + idxLocalAssetChecksum, + uQRemoteAssetOwnerChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAlbumAssetEntity, + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity + ]; + @override + int get schemaVersion => 3; + @override + DriftDatabaseOptions get options => const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v4.dart b/mobile/test/drift/main/generated/schema_v4.dart new file mode 100644 index 0000000000..f54e5e9644 --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v4.dart @@ -0,0 +1,5003 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isAdmin = GeneratedColumn('is_admin', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn email = + GeneratedColumn('email', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn profileImagePath = GeneratedColumn('profile_image_path', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn('quota_size_in_bytes', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn('quota_usage_in_bytes', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: const CustomExpression('0')); + @override + List get $columns => + [id, name, isAdmin, email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + isAdmin: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_admin'])!, + email: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}email'])!, + profileImagePath: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}profile_image_path']), + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + quotaSizeInBytes: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}quota_size_in_bytes']), + quotaUsageInBytes: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}quota_usage_in_bytes'])!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final bool isAdmin; + final String email; + final String? profileImagePath; + final DateTime updatedAt; + final int? quotaSizeInBytes; + final int quotaUsageInBytes; + const UserEntityData( + {required this.id, + required this.name, + required this.isAdmin, + required this.email, + this.profileImagePath, + required this.updatedAt, + this.quotaSizeInBytes, + required this.quotaUsageInBytes}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['is_admin'] = Variable(isAdmin); + map['email'] = Variable(email); + if (!nullToAbsent || profileImagePath != null) { + map['profile_image_path'] = Variable(profileImagePath); + } + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || quotaSizeInBytes != null) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + } + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + return map; + } + + factory UserEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + isAdmin: serializer.fromJson(json['isAdmin']), + email: serializer.fromJson(json['email']), + profileImagePath: serializer.fromJson(json['profileImagePath']), + updatedAt: serializer.fromJson(json['updatedAt']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'isAdmin': serializer.toJson(isAdmin), + 'email': serializer.toJson(email), + 'profileImagePath': serializer.toJson(profileImagePath), + 'updatedAt': serializer.toJson(updatedAt), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + }; + } + + UserEntityData copyWith( + {String? id, + String? name, + bool? isAdmin, + String? email, + Value profileImagePath = const Value.absent(), + DateTime? updatedAt, + Value quotaSizeInBytes = const Value.absent(), + int? quotaUsageInBytes}) => + UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath.present ? profileImagePath.value : this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes.present ? quotaSizeInBytes.value : this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + email: data.email.present ? data.email.value : this.email, + profileImagePath: data.profileImagePath.present ? data.profileImagePath.value : this.profileImagePath, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + quotaSizeInBytes: data.quotaSizeInBytes.present ? data.quotaSizeInBytes.value : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present ? data.quotaUsageInBytes.value : this.quotaUsageInBytes, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, isAdmin, email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.isAdmin == this.isAdmin && + other.email == this.email && + other.profileImagePath == this.profileImagePath && + other.updatedAt == this.updatedAt && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value isAdmin; + final Value email; + final Value profileImagePath; + final Value updatedAt; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.isAdmin = const Value.absent(), + this.email = const Value.absent(), + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + this.isAdmin = const Value.absent(), + required String email, + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? isAdmin, + Expression? email, + Expression? profileImagePath, + Expression? updatedAt, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (isAdmin != null) 'is_admin': isAdmin, + if (email != null) 'email': email, + if (profileImagePath != null) 'profile_image_path': profileImagePath, + if (updatedAt != null) 'updated_at': updatedAt, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + }); + } + + UserEntityCompanion copyWith( + {Value? id, + Value? name, + Value? isAdmin, + Value? email, + Value? profileImagePath, + Value? updatedAt, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes}) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath ?? this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (profileImagePath.present) { + map['profile_image_path'] = Variable(profileImagePath.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = + GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = + GeneratedColumn('width', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = + GeneratedColumn('height', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn('duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = + GeneratedColumn('checksum', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn('is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn localDateTime = GeneratedColumn('local_date_time', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn thumbHash = + GeneratedColumn('thumb_hash', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn deletedAt = GeneratedColumn('deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn visibility = + GeneratedColumn('visibility', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn stackId = + GeneratedColumn('stack_id', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}checksum'])!, + isFavorite: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + localDateTime: + attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}local_date_time']), + thumbHash: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumb_hash']), + deletedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + livePhotoVideoId: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}live_photo_video_id']), + visibility: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}visibility'])!, + stackId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}stack_id']), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + const RemoteAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + }; + } + + RemoteAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent()}) => + RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present ? durationInSeconds.value : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present ? localDateTime.value : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present ? livePhotoVideoId.value : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present ? data.durationInSeconds.value : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present ? data.localDateTime.value : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present ? data.livePhotoVideoId.value : this.livePhotoVideoId, + visibility: data.visibility.present ? data.visibility.value : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, type, createdAt, updatedAt, width, height, durationInSeconds, id, checksum, + isFavorite, ownerId, localDateTime, thumbHash, deletedAt, livePhotoVideoId, visibility, stackId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId); +} + +class RemoteAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + }); + } + + RemoteAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId}) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn primaryAssetId = GeneratedColumn('primary_asset_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, updatedAt, ownerId, primaryAssetId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + primaryAssetId: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith( + {String? id, DateTime? createdAt, DateTime? updatedAt, String? ownerId, String? primaryAssetId}) => + StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present ? data.primaryAssetId.value : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId}) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = + GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = + GeneratedColumn('width', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = + GeneratedColumn('height', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn('duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = + GeneratedColumn('checksum', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn isFavorite = GeneratedColumn('is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn orientation = GeneratedColumn('orientation', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: const CustomExpression('0')); + @override + List get $columns => + [name, type, createdAt, updatedAt, width, height, durationInSeconds, id, checksum, isFavorite, orientation]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: + attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}checksum']), + isFavorite: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + orientation: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}orientation'])!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + const LocalAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + return map; + } + + factory LocalAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + }; + } + + LocalAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation}) => + LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present ? durationInSeconds.value : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + LocalAssetEntityData copyWithCompanion(LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present ? data.durationInSeconds.value : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + orientation: data.orientation.present ? data.orientation.value : this.orientation, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, type, createdAt, updatedAt, width, height, durationInSeconds, id, checksum, isFavorite, orientation); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + }); + } + + LocalAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation}) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn backupSelection = + GeneratedColumn('backup_selection', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn('is_ios_shared_album', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_ios_shared_album" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn marker_ = GeneratedColumn('marker', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))')); + @override + List get $columns => [id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + backupSelection: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}backup_selection'])!, + isIosSharedAlbum: + attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_ios_shared_album'])!, + marker_: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}marker']), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final bool? marker_; + const LocalAlbumEntityData( + {required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.marker_}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith( + {String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value marker_ = const Value.absent()}) => + LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present ? data.backupSelection.value : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present ? data.isIosSharedAlbum.value : this.isIosSharedAlbum, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? marker_}) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES local_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn('album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES local_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass implements Insertable { + final String assetId; + final String albumId; + const LocalAlbumAssetEntityData({required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion(LocalAlbumAssetEntityCompanion data) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && other.assetId == this.assetId && other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({Value? assetId, Value? albumId}) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn('user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn key = + GeneratedColumn('key', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn value = + GeneratedColumn('value', aliasedName, false, type: DriftSqlType.blob, requiredDuringInsert: true); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + key: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}key'])!, + value: attachedDatabase.typeMapping.read(DriftSqlType.blob, data['${effectivePrefix}value'])!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({required this.userId, required this.key, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({String? userId, int? key, Uint8List? value}) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({Value? userId, Value? key, Value? value}) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn('shared_by_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn sharedWithId = GeneratedColumn('shared_with_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn inTimeline = GeneratedColumn('in_timeline', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("in_timeline" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}shared_by_id'])!, + sharedWithId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}shared_with_id'])!, + inTimeline: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({required this.sharedById, required this.sharedWithId, required this.inTimeline}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({String? sharedById, String? sharedWithId, bool? inTimeline}) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present ? data.sharedById.value : this.sharedById, + sharedWithId: data.sharedWithId.present ? data.sharedWithId.value : this.sharedWithId, + inTimeline: data.inTimeline.present ? data.inTimeline.value : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({Value? sharedById, Value? sharedWithId, Value? inTimeline}) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn city = + GeneratedColumn('city', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn state = + GeneratedColumn('state', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn country = + GeneratedColumn('country', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn dateTimeOriginal = GeneratedColumn( + 'date_time_original', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn description = + GeneratedColumn('description', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn height = + GeneratedColumn('height', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn width = + GeneratedColumn('width', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn exposureTime = GeneratedColumn('exposure_time', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn fNumber = + GeneratedColumn('f_number', aliasedName, true, type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn fileSize = + GeneratedColumn('file_size', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn focalLength = GeneratedColumn('focal_length', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn latitude = + GeneratedColumn('latitude', aliasedName, true, type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn longitude = + GeneratedColumn('longitude', aliasedName, true, type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn iso = + GeneratedColumn('iso', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn make = + GeneratedColumn('make', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn model = + GeneratedColumn('model', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn lens = + GeneratedColumn('lens', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn orientation = + GeneratedColumn('orientation', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn timeZone = + GeneratedColumn('time_zone', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn rating = + GeneratedColumn('rating', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn projectionType = GeneratedColumn('projection_type', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + city: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}city']), + state: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}state']), + country: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}country']), + dateTimeOriginal: + attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}date_time_original']), + description: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}description']), + height: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}height']), + width: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}width']), + exposureTime: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}exposure_time']), + fNumber: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}f_number']), + fileSize: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}file_size']), + focalLength: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}focal_length']), + latitude: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}latitude']), + longitude: attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}longitude']), + iso: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}iso']), + make: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}make']), + model: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}model']), + lens: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}lens']), + orientation: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}orientation']), + timeZone: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}time_zone']), + rating: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}rating']), + projectionType: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}projection_type']), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData( + {required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson(json['dateTimeOriginal']), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith( + {String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent()}) => + RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present ? dateTimeOriginal.value : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present ? projectionType.value : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present ? data.dateTimeOriginal.value : this.dateTimeOriginal, + description: data.description.present ? data.description.value : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present ? data.exposureTime.value : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present ? data.focalLength.value : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present ? data.orientation.value : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present ? data.projectionType.value : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith( + {Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType}) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn('description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: false, defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn('thumbnail_asset_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE SET NULL')); + late final GeneratedColumn isActivityEnabled = GeneratedColumn('is_activity_enabled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_activity_enabled" IN (0, 1))'), + defaultValue: const CustomExpression('1')); + late final GeneratedColumn order = + GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => + [id, name, description, createdAt, updatedAt, ownerId, thumbnailAssetId, isActivityEnabled, order]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}description'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + thumbnailAssetId: + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumbnail_asset_id']), + isActivityEnabled: + attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_activity_enabled'])!, + order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData( + {required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith( + {String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order}) => + RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present ? thumbnailAssetId.value : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present ? data.description.value : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present ? data.thumbnailAssetId.value : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present ? data.isActivityEnabled.value : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, description, createdAt, updatedAt, ownerId, thumbnailAssetId, isActivityEnabled, order); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order}) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn('album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion(RemoteAlbumAssetEntityCompanion data) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && other.assetId == this.assetId && other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({Value? assetId, Value? albumId}) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn('album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn userId = GeneratedColumn('user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn role = + GeneratedColumn('role', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + userId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + role: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}role'])!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({required this.albumId, required this.userId, required this.role}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({String? albumId, String? userId, int? role}) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion(RemoteAlbumUserEntityCompanion data) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({Value? albumId, Value? userId, Value? role}) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn('deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn type = + GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn data = + GeneratedColumn('data', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isSaved = GeneratedColumn('is_saved', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_saved" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn memoryAt = GeneratedColumn('memory_at', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn seenAt = + GeneratedColumn('seen_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn showAt = + GeneratedColumn('show_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn hideAt = + GeneratedColumn('hide_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => + [id, createdAt, updatedAt, deletedAt, ownerId, type, data, isSaved, memoryAt, seenAt, showAt, hideAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + deletedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, + data: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}data'])!, + isSaved: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_saved'])!, + memoryAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}memory_at'])!, + seenAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}seen_at']), + showAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}show_at']), + hideAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}hide_at']), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent()}) => + MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, deletedAt, ownerId, type, data, isSaved, memoryAt, seenAt, showAt, hideAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt}) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn memoryId = GeneratedColumn('memory_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES memory_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + memoryId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}memory_id'])!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && other.assetId == this.assetId && other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({Value? assetId, Value? memoryId}) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn('created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn('updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn('owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn faceAssetId = GeneratedColumn('face_asset_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn isFavorite = GeneratedColumn('is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))')); + late final GeneratedColumn isHidden = GeneratedColumn('is_hidden', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_hidden" IN (0, 1))')); + late final GeneratedColumn color = + GeneratedColumn('color', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn birthDate = GeneratedColumn('birth_date', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => + [id, createdAt, updatedAt, ownerId, name, faceAssetId, isFavorite, isHidden, color, birthDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, + faceAssetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}face_asset_id']), + isFavorite: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + isHidden: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_hidden'])!, + color: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}color']), + birthDate: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}birth_date']), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent()}) => + PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present ? data.faceAssetId.value : this.faceAssetId, + isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, name, faceAssetId, isFavorite, isHidden, color, birthDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate}) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn assetId = GeneratedColumn('asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn personId = GeneratedColumn('person_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES person_entity (id) ON DELETE SET NULL')); + late final GeneratedColumn imageWidth = + GeneratedColumn('image_width', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn imageHeight = + GeneratedColumn('image_height', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn boundingBoxX1 = + GeneratedColumn('bounding_box_x1', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn boundingBoxY1 = + GeneratedColumn('bounding_box_y1', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn boundingBoxX2 = + GeneratedColumn('bounding_box_x2', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn boundingBoxY2 = + GeneratedColumn('bounding_box_y2', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn sourceType = + GeneratedColumn('source_type', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + assetId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + personId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}person_id']), + imageWidth: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}image_width'])!, + imageHeight: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}image_height'])!, + boundingBoxX1: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}bounding_box_x1'])!, + boundingBoxY1: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}bounding_box_y1'])!, + boundingBoxX2: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}bounding_box_x2'])!, + boundingBoxY2: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}bounding_box_y2'])!, + sourceType: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}source_type'])!, + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData( + {required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + AssetFaceEntityData copyWith( + {String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType}) => + AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present ? data.imageWidth.value : this.imageWidth, + imageHeight: data.imageHeight.present ? data.imageHeight.value : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present ? data.boundingBoxX1.value : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present ? data.boundingBoxY1.value : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present ? data.boundingBoxX2.value : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present ? data.boundingBoxY2.value : this.boundingBoxY2, + sourceType: data.sourceType.present ? data.sourceType.value : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, assetId, personId, imageWidth, imageHeight, boundingBoxX1, boundingBoxY1, + boundingBoxX2, boundingBoxY2, sourceType); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + AssetFaceEntityCompanion copyWith( + {Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType}) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV4 extends GeneratedDatabase { + DatabaseAtV4(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = LocalAlbumAssetEntity(this); + late final Index idxLocalAssetChecksum = + Index('idx_local_asset_checksum', 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + late final Index uQRemoteAssetOwnerChecksum = Index('UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + late final Index idxRemoteAssetChecksum = + Index('idx_remote_asset_checksum', 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = RemoteAlbumUserEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + @override + Iterable> get allTables => allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + uQRemoteAssetOwnerChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity + ]; + @override + int get schemaVersion => 4; + @override + DriftDatabaseOptions get options => const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/migration_test.dart b/mobile/test/drift/main/migration_test.dart new file mode 100644 index 0000000000..74467492ae --- /dev/null +++ b/mobile/test/drift/main/migration_test.dart @@ -0,0 +1,38 @@ +// dart format width=80 +// ignore_for_file: unused_local_variable, unused_import +import 'package:drift/drift.dart'; +import 'package:drift_dev/api/migrations_native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +import 'generated/schema.dart'; +import 'generated/schema_v1.dart' as v1; +import 'generated/schema_v2.dart' as v2; + +void main() { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + late SchemaVerifier verifier; + + setUpAll(() { + verifier = SchemaVerifier(GeneratedHelper()); + }); + + group('simple database migrations', () { + // These simple tests verify all possible schema updates with a simple (no + // data) migration. This is a quick way to ensure that written database + // migrations properly alter the schema. + const versions = GeneratedHelper.versions; + for (final (i, fromVersion) in versions.indexed) { + group('from $fromVersion', () { + for (final toVersion in versions.skip(i + 1)) { + test('to $toVersion', () async { + final schema = await verifier.schemaAt(fromVersion); + final db = Drift(schema.newConnection()); + await verifier.migrateAndValidate(db, toVersion); + await db.close(); + }); + } + }); + } + }); +} diff --git a/mobile/test/fixtures/album.stub.dart b/mobile/test/fixtures/album.stub.dart index 1432d35901..1e79f62faf 100644 --- a/mobile/test/fixtures/album.stub.dart +++ b/mobile/test/fixtures/album.stub.dart @@ -1,4 +1,4 @@ -import 'package:immich_mobile/domain/models/local_album.model.dart'; +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'; diff --git a/mobile/test/fixtures/sync_stream.stub.dart b/mobile/test/fixtures/sync_stream.stub.dart index ba97f1434a..6c47b9172e 100644 --- a/mobile/test/fixtures/sync_stream.stub.dart +++ b/mobile/test/fixtures/sync_stream.stub.dart @@ -9,6 +9,7 @@ abstract final class SyncStreamStub { email: "admin@admin", id: "1", name: "Admin", + avatarColor: null, ), ack: "1", ); @@ -19,6 +20,7 @@ abstract final class SyncStreamStub { email: "user@user", id: "5", name: "User", + avatarColor: null, ), ack: "5", ); @@ -42,4 +44,47 @@ abstract final class SyncStreamStub { data: SyncPartnerDeleteV1(sharedById: "3", sharedWithId: "4"), ack: "4", ); + + static final memoryV1 = SyncEvent( + type: SyncEntityType.memoryV1, + data: SyncMemoryV1( + createdAt: DateTime(2023, 1, 1), + data: {"year": 2023, "title": "Test Memory"}, + deletedAt: null, + hideAt: null, + id: "memory-1", + isSaved: false, + memoryAt: DateTime(2023, 1, 1), + ownerId: "user-1", + seenAt: null, + showAt: DateTime(2023, 1, 1), + type: MemoryType.onThisDay, + updatedAt: DateTime(2023, 1, 1), + ), + ack: "5", + ); + + static final memoryDeleteV1 = SyncEvent( + type: SyncEntityType.memoryDeleteV1, + data: SyncMemoryDeleteV1(memoryId: "memory-2"), + ack: "6", + ); + + static final memoryToAssetV1 = SyncEvent( + type: SyncEntityType.memoryToAssetV1, + data: SyncMemoryAssetV1( + assetId: "asset-1", + memoryId: "memory-1", + ), + ack: "7", + ); + + static final memoryToAssetDeleteV1 = SyncEvent( + type: SyncEntityType.memoryToAssetDeleteV1, + data: SyncMemoryAssetDeleteV1( + assetId: "asset-2", + memoryId: "memory-1", + ), + ack: "8", + ); } diff --git a/mobile/test/infrastructure/repositories/exif_repository_test.dart b/mobile/test/infrastructure/repositories/exif_repository_test.dart index e267d2dac4..4e7ee4d79d 100644 --- a/mobile/test/infrastructure/repositories/exif_repository_test.dart +++ b/mobile/test/infrastructure/repositories/exif_repository_test.dart @@ -1,5 +1,4 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; import 'package:isar/isar.dart'; @@ -20,7 +19,7 @@ Future _populateExifTable(Isar db) async { void main() { late Isar db; - late IExifInfoRepository sut; + late IsarExifRepository sut; setUp(() async { db = await TestUtils.initIsar(); diff --git a/mobile/test/infrastructure/repositories/local_album_repository_test.dart b/mobile/test/infrastructure/repositories/local_album_repository_test.dart index 827d81c79b..bb4292be07 100644 --- a/mobile/test/infrastructure/repositories/local_album_repository_test.dart +++ b/mobile/test/infrastructure/repositories/local_album_repository_test.dart @@ -1,9 +1,9 @@ import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; -import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import '../../test_utils/medium_factory.dart'; @@ -23,8 +23,7 @@ void main() { group('getAll', () { test('sorts albums by backupSelection & isIosSharedAlbum', () async { - final localAlbumRepo = - mediumFactory.getRepository(); + final localAlbumRepo = mediumFactory.getRepository(); await localAlbumRepo.upsert( mediumFactory.localAlbum( id: '1', diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 528e17ba3d..6d75fbc765 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -1,5 +1,4 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/interfaces/store.interface.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'; @@ -42,7 +41,7 @@ Future _populateStore(Isar db) async { void main() { late Isar db; - late IStoreRepository sut; + late IsarStoreRepository sut; setUp(() async { db = await TestUtils.initIsar(); @@ -67,8 +66,7 @@ void main() { }); test('converts datetime', () async { - DateTime? backupFailedSince = - await sut.tryGet(StoreKey.backupFailedSince); + DateTime? backupFailedSince = await sut.tryGet(StoreKey.backupFailedSince); expect(backupFailedSince, isNull); await sut.insert(StoreKey.backupFailedSince, _kTestBackupFailed); backupFailedSince = await sut.tryGet(StoreKey.backupFailedSince); diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index 55b03a8116..3348cc7c04 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -39,23 +39,19 @@ void main() { mockSyncApi = MockSyncApi(); mockHttpClient = MockHttpClient(); mockStreamedResponse = MockStreamedResponse(); - responseStreamController = - StreamController>.broadcast(sync: true); + responseStreamController = StreamController>.broadcast(sync: true); registerFallbackValue(FakeBaseRequest()); when(() => mockApiService.apiClient).thenReturn(mockApiClient); when(() => mockApiService.syncApi).thenReturn(mockSyncApi); when(() => mockApiClient.basePath).thenReturn('http://demo.immich.app/api'); - when(() => mockApiService.applyToParams(any(), any())) - .thenAnswer((_) async => {}); + when(() => mockApiService.applyToParams(any(), any())).thenAnswer((_) async => {}); // Mock HTTP client behavior - when(() => mockHttpClient.send(any())) - .thenAnswer((_) async => mockStreamedResponse); + when(() => mockHttpClient.send(any())).thenAnswer((_) async => mockStreamedResponse); when(() => mockStreamedResponse.statusCode).thenReturn(200); - when(() => mockStreamedResponse.stream) - .thenAnswer((_) => http.ByteStream(responseStreamController.stream)); + when(() => mockStreamedResponse.stream).thenAnswer((_) => http.ByteStream(responseStreamController.stream)); when(() => mockHttpClient.close()).thenAnswer((_) => {}); sut = SyncApiRepository(mockApiService); @@ -270,8 +266,7 @@ void main() { test('streamChanges throws ApiException on non-200 status code', () async { when(() => mockStreamedResponse.statusCode).thenReturn(401); final errorBodyController = StreamController>(sync: true); - when(() => mockStreamedResponse.stream) - .thenAnswer((_) => http.ByteStream(errorBodyController.stream)); + when(() => mockStreamedResponse.stream).thenAnswer((_) => http.ByteStream(errorBodyController.stream)); int onDataCallCount = 0; diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index e41b89f16a..ed20f177b7 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -1,33 +1,32 @@ -import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart'; -import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; -import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart'; -import 'package:immich_mobile/domain/interfaces/log.interface.dart'; -import 'package:immich_mobile/domain/interfaces/storage.interface.dart'; -import 'package:immich_mobile/domain/interfaces/store.interface.dart'; -import 'package:immich_mobile/domain/interfaces/sync_api.interface.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'; +import 'package:immich_mobile/infrastructure/repositories/storage.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/infrastructure/repositories/sync_stream.repository.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'; -class MockStoreRepository extends Mock implements IStoreRepository {} +class MockStoreRepository extends Mock implements IsarStoreRepository {} -class MockLogRepository extends Mock implements ILogRepository {} +class MockLogRepository extends Mock implements IsarLogRepository {} class MockIsarUserRepository extends Mock implements IsarUserRepository {} -class MockDeviceAssetRepository extends Mock - implements IDeviceAssetRepository {} +class MockDeviceAssetRepository extends Mock implements IsarDeviceAssetRepository {} class MockSyncStreamRepository extends Mock implements SyncStreamRepository {} -class MockLocalAlbumRepository extends Mock implements ILocalAlbumRepository {} +class MockLocalAlbumRepository extends Mock implements DriftLocalAlbumRepository {} -class MockLocalAssetRepository extends Mock implements ILocalAssetRepository {} +class MockLocalAssetRepository extends Mock implements DriftLocalAssetRepository {} -class MockStorageRepository extends Mock implements IStorageRepository {} +class MockStorageRepository extends Mock implements StorageRepository {} // API Repos class MockUserApiRepository extends Mock implements UserApiRepository {} -class MockSyncApiRepository extends Mock implements ISyncApiRepository {} +class MockSyncApiRepository extends Mock implements SyncApiRepository {} diff --git a/mobile/test/mock_http_override.dart b/mobile/test/mock_http_override.dart index c25fb79b50..ae19d932c0 100644 --- a/mobile/test/mock_http_override.dart +++ b/mobile/test/mock_http_override.dart @@ -18,15 +18,12 @@ class MockHttpOverrides extends HttpOverrides { // Request mocks when(() => request.headers).thenAnswer((_) => headers); - when(() => request.close()) - .thenAnswer((_) => Future.value(response)); + when(() => request.close()).thenAnswer((_) => Future.value(response)); // Response mocks when(() => response.statusCode).thenReturn(HttpStatus.ok); - when(() => response.compressionState) - .thenReturn(HttpClientResponseCompressionState.decompressed); - when(() => response.contentLength) - .thenAnswer((_) => kTransparentImage.length); + when(() => response.compressionState).thenReturn(HttpClientResponseCompressionState.decompressed); + when(() => response.contentLength).thenAnswer((_) => kTransparentImage.length); when( () => response.listen( captureAny(), @@ -35,18 +32,15 @@ class MockHttpOverrides extends HttpOverrides { onError: captureAny(named: 'onError'), ), ).thenAnswer((invocation) { - final onData = - invocation.positionalArguments[0] as void Function(List); + final onData = invocation.positionalArguments[0] as void Function(List); final onDone = invocation.namedArguments[#onDone] as void Function(); - final onError = invocation.namedArguments[#onError] as void - Function(Object, [StackTrace]); + final onError = invocation.namedArguments[#onError] as void Function(Object, [StackTrace]); final cancelOnError = invocation.namedArguments[#cancelOnError] as bool; - return Stream>.fromIterable([kTransparentImage.toList()]) - .listen( + return Stream>.fromIterable([kTransparentImage.toList()]).listen( onData, onDone: onDone, onError: onError, diff --git a/mobile/test/modules/activity/activities_page_test.dart b/mobile/test/modules/activity/activities_page_test.dart index cf9238d205..c3279e9b58 100644 --- a/mobile/test/modules/activity/activities_page_test.dart +++ b/mobile/test/modules/activity/activities_page_test.dart @@ -198,8 +198,7 @@ void main() { ); await tester.pumpAndSettle(); - when(() => activityMock.addComment(any())) - .thenAnswer((_) => Future.value()); + when(() => activityMock.addComment(any())).thenAnswer((_) => Future.value()); final textField = find.byType(TextField); await tester.enterText(textField, 'Test comment'); diff --git a/mobile/test/modules/activity/activity_mocks.dart b/mobile/test/modules/activity/activity_mocks.dart index 22fbafdbf3..c50810795e 100644 --- a/mobile/test/modules/activity/activity_mocks.dart +++ b/mobile/test/modules/activity/activity_mocks.dart @@ -6,9 +6,7 @@ import 'package:mocktail/mocktail.dart'; class ActivityServiceMock extends Mock implements ActivityService {} -class MockAlbumActivity extends AlbumActivityInternal - with Mock - implements AlbumActivity { +class MockAlbumActivity extends AlbumActivityInternal with Mock implements AlbumActivity { List? initActivities; MockAlbumActivity([this.initActivities]); @@ -18,6 +16,4 @@ class MockAlbumActivity extends AlbumActivityInternal } } -class ActivityStatisticsMock extends ActivityStatisticsInternal - with Mock - implements ActivityStatistics {} +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 index a3b3e2466e..9bac84bab9 100644 --- a/mobile/test/modules/activity/activity_provider_test.dart +++ b/mobile/test/modules/activity/activity_provider_test.dart @@ -58,8 +58,7 @@ void main() { container = TestUtils.createContainer( overrides: [ activityServiceProvider.overrideWith((ref) => activityMock), - activityStatisticsProvider('test-album', 'test-asset') - .overrideWith(() => activityStatisticsMock), + activityStatisticsProvider('test-album', 'test-asset').overrideWith(() => activityStatisticsMock), ], ); @@ -90,8 +89,7 @@ void main() { [ isA>>(), predicate( - (AsyncData> ad) => - ad.requireValue.every((e) => _activities.contains(e)), + (AsyncData> ad) => ad.requireValue.every((e) => _activities.contains(e)), ), ], ), @@ -172,8 +170,7 @@ void main() { group('removeActivity()', () { test('Like successfully removed', () async { - when(() => activityMock.removeActivity('3')) - .thenAnswer((_) async => true); + when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true); await container.read(provider.notifier).removeActivity('3'); @@ -192,8 +189,7 @@ void main() { }); test('Remove Like failed', () async { - when(() => activityMock.removeActivity('3')) - .thenAnswer((_) async => false); + when(() => activityMock.removeActivity('3')).thenAnswer((_) async => false); await container.read(provider.notifier).removeActivity('3'); @@ -206,8 +202,7 @@ void main() { }); test('Comment successfully removed', () async { - when(() => activityMock.removeActivity('1')) - .thenAnswer((_) async => true); + when(() => activityMock.removeActivity('1')).thenAnswer((_) async => true); await container.read(provider.notifier).removeActivity('1'); @@ -229,10 +224,8 @@ void main() { container = TestUtils.createContainer( overrides: [ activityServiceProvider.overrideWith((ref) => activityMock), - activityStatisticsProvider('test-album', 'test-asset') - .overrideWith(() => activityStatisticsMock), - activityStatisticsProvider('test-album') - .overrideWith(() => albumActivityStatisticsMock), + activityStatisticsProvider('test-album', 'test-asset').overrideWith(() => activityStatisticsMock), + activityStatisticsProvider('test-album').overrideWith(() => albumActivityStatisticsMock), ], ); }); @@ -255,8 +248,7 @@ void main() { comment: 'Test-Comment', ), ).thenAnswer((_) async => AsyncData(comment)); - when(() => activityStatisticsMock.build('test-album', 'test-asset')) - .thenReturn(4); + when(() => activityStatisticsMock.build('test-album', 'test-asset')).thenReturn(4); when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); await container.read(provider.notifier).addComment('Test-Comment'); @@ -296,8 +288,7 @@ void main() { ), ).thenAnswer((_) async => AsyncData(comment)); when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); - when(() => activityMock.getAllActivities('test-album')) - .thenAnswer((_) async => [..._activities]); + when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); final albumProvider = albumActivityProvider('test-album'); await container.read(albumProvider.notifier).addComment('Test-Comment'); diff --git a/mobile/test/modules/activity/activity_text_field_test.dart b/mobile/test/modules/activity/activity_text_field_test.dart index a124af0db9..e74099cdcd 100644 --- a/mobile/test/modules/activity/activity_text_field_test.dart +++ b/mobile/test/modules/activity/activity_text_field_test.dart @@ -44,8 +44,7 @@ void main() { activityMock = MockAlbumActivity(); overrides = [ currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), - albumActivityProvider(AlbumStub.twoAsset.remoteId!) - .overrideWith(() => activityMock), + albumActivityProvider(AlbumStub.twoAsset.remoteId!).overrideWith(() => activityMock), ]; }); @@ -152,8 +151,7 @@ void main() { overrides: overrides, ); - when(() => activityMock.removeActivity(any())) - .thenAnswer((_) => Future.value()); + when(() => activityMock.removeActivity(any())).thenAnswer((_) => Future.value()); final suffixIcon = find.byType(IconButton); await tester.tap(suffixIcon); diff --git a/mobile/test/modules/activity/activity_tile_test.dart b/mobile/test/modules/activity/activity_tile_test.dart index 22dd606540..fddbb6269c 100644 --- a/mobile/test/modules/activity/activity_tile_test.dart +++ b/mobile/test/modules/activity/activity_tile_test.dart @@ -57,8 +57,7 @@ void main() { expect(find.byType(ListTile), findsOneWidget); }); - testWidgets('No trailing widget when activity assetId == null', - (tester) async { + testWidgets('No trailing widget when activity assetId == null', (tester) async { await tester.pumpConsumerWidget( ActivityTile( Activity( @@ -75,9 +74,7 @@ void main() { expect(listTile.trailing, isNull); }); - testWidgets( - 'Asset Thumbanil as trailing widget when activity assetId != null', - (tester) async { + testWidgets('Asset Thumbanil as trailing widget when activity assetId != null', (tester) async { await tester.pumpConsumerWidget( ActivityTile( Activity( @@ -176,8 +173,7 @@ void main() { user: UserStub.admin, ); - testWidgets('Comment contains User Circle Avatar as leading', - (tester) async { + testWidgets('Comment contains User Circle Avatar as leading', (tester) async { await tester.pumpConsumerWidget( ActivityTile(activity), overrides: overrides, diff --git a/mobile/test/modules/activity/dismissible_activity_test.dart b/mobile/test/modules/activity/dismissible_activity_test.dart index 7bfa400a37..8cc81a69cf 100644 --- a/mobile/test/modules/activity/dismissible_activity_test.dart +++ b/mobile/test/modules/activity/dismissible_activity_test.dart @@ -55,9 +55,7 @@ void main() { expect(find.byType(ConfirmDialog), findsOneWidget); }); - testWidgets( - 'Ok action in ConfirmDialog should call onDismiss with activityId', - (tester) async { + testWidgets('Ok action in ConfirmDialog should call onDismiss with activityId', (tester) async { String? receivedActivityId; await tester.pumpConsumerWidget( DismissibleActivity( diff --git a/mobile/test/modules/album/album_mocks.dart b/mobile/test/modules/album/album_mocks.dart index 147d7b4221..7a1b76e0c7 100644 --- a/mobile/test/modules/album/album_mocks.dart +++ b/mobile/test/modules/album/album_mocks.dart @@ -2,9 +2,7 @@ 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 { +class MockCurrentAlbumProvider extends CurrentAlbum with Mock implements CurrentAlbumInternal { Album? initAlbum; MockCurrentAlbumProvider([this.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 index df59f03c56..bf3163f00d 100644 --- a/mobile/test/modules/album/album_sort_by_options_provider_test.dart +++ b/mobile/test/modules/album/album_sort_by_options_provider_test.dart @@ -261,9 +261,7 @@ void main() { }); test('Properly saves the correct store index of sort mode', () { - container - .read(albumSortByOptionsProvider.notifier) - .changeSortMode(AlbumSortMode.mostOldest); + container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest); verify( () => settingsMock.setSetting( @@ -286,14 +284,10 @@ void main() { ); // Created -> Most Oldest - container - .read(albumSortByOptionsProvider.notifier) - .changeSortMode(AlbumSortMode.mostOldest); + container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest); // Most Oldest -> Title - container - .read(albumSortByOptionsProvider.notifier) - .changeSortMode(AlbumSortMode.title); + container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.title); verifyInOrder([ () => listener.call(null, AlbumSortMode.created), @@ -368,9 +362,7 @@ void main() { container.read(albumSortOrderProvider.notifier).changeSortDirection(true); // true -> false - container - .read(albumSortOrderProvider.notifier) - .changeSortDirection(false); + container.read(albumSortOrderProvider.notifier).changeSortDirection(false); verifyInOrder([ () => listener.call(null, false), diff --git a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart b/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart index f81f5a9a19..89b06d3c09 100644 --- a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart +++ b/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart @@ -2,9 +2,7 @@ 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 { +class MockCurrentAssetProvider extends CurrentAssetInternal with Mock implements CurrentAsset { Asset? initAsset; MockCurrentAssetProvider([this.initAsset]); diff --git a/mobile/test/modules/extensions/asset_extensions_test.dart b/mobile/test/modules/extensions/asset_extensions_test.dart index dd334c7b9d..a164b9ddce 100644 --- a/mobile/test/modules/extensions/asset_extensions_test.dart +++ b/mobile/test/modules/extensions/asset_extensions_test.dart @@ -76,8 +76,7 @@ void main() { expect(dateTimeInUTC.timeZoneOffset, tz); }); - test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone', - () { + 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( @@ -98,13 +97,11 @@ void main() { 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 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)); + final adjustedTime = TZDateTime.from(dateTimeOriginal.toUtc(), getLocation(location)); expect(adjustedTime, dt); expect(adjustedTime.timeZoneOffset, tz); }); @@ -118,8 +115,7 @@ void main() { final (dt, tz) = a.getTZAdjustedTimeAndOffset(); final location = getLocation("Asia/Hong_Kong"); - final offsetFromLocation = - Duration(milliseconds: location.currentTimeZone.offset); + 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 diff --git a/mobile/test/modules/extensions/datetime_extensions_test.dart b/mobile/test/modules/extensions/datetime_extensions_test.dart new file mode 100644 index 0000000000..b1b1542fcd --- /dev/null +++ b/mobile/test/modules/extensions/datetime_extensions_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; +import 'package:intl/date_symbol_data_local.dart'; + +void main() { + setUpAll(() async { + await initializeDateFormatting(); + }); + + group('DateRangeFormatting.formatDateRange', () { + final currentYear = DateTime.now().year; + + test('returns single date format for this year', () { + final date = DateTime(currentYear, 8, 28); // Aug 28 this year + final result = DateRangeFormatting.formatDateRange(date, date, null); + expect(result, 'Aug 28'); + }); + + test('returns single date format for other year', () { + final date = DateTime(2023, 8, 28); // Aug 28, 2023 + final result = DateRangeFormatting.formatDateRange(date, date, null); + expect(result, 'Aug 28, 2023'); + }); + + test('returns date range format for this year', () { + final startDate = DateTime(currentYear, 3, 23); // Mar 23 + final endDate = DateTime(currentYear, 5, 31); // May 31 + final result = DateRangeFormatting.formatDateRange(startDate, endDate, null); + expect(result, 'Mar 23 - May 31'); + }); + + test('returns date range format for other year (same year)', () { + final startDate = DateTime(2023, 8, 28); // Aug 28 + final endDate = DateTime(2023, 9, 30); // Sep 30 + final result = DateRangeFormatting.formatDateRange(startDate, endDate, null); + expect(result, 'Aug 28 - Sep 30, 2023'); + }); + + test('returns date range format over multiple years', () { + final startDate = DateTime(2021, 4, 17); // Apr 17, 2021 + final endDate = DateTime(2022, 4, 9); // Apr 9, 2022 + final result = DateRangeFormatting.formatDateRange(startDate, endDate, null); + expect(result, 'Apr 17, 2021 - Apr 9, 2022'); + }); + }); +} diff --git a/mobile/test/modules/map/map_mocks.dart b/mobile/test/modules/map/map_mocks.dart index cb525b2d17..959cad3da6 100644 --- a/mobile/test/modules/map/map_mocks.dart +++ b/mobile/test/modules/map/map_mocks.dart @@ -3,9 +3,7 @@ import 'package:immich_mobile/models/map/map_state.model.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:mocktail/mocktail.dart'; -class MockMapStateNotifier extends Notifier - with Mock - implements MapStateNotifier { +class MockMapStateNotifier extends Notifier with Mock implements MapStateNotifier { final MapState initState; MockMapStateNotifier(this.initState); diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart index 5a6b163c04..78fa6b0043 100644 --- a/mobile/test/modules/map/map_theme_override_test.dart +++ b/mobile/test/modules/map/map_theme_override_test.dart @@ -1,3 +1,4 @@ +@Skip('Flaky test, needs investigation') @Tags(['widget']) library; @@ -23,12 +24,12 @@ void main() { late Isar db; setUpAll(() async { - TestUtils.init(); db = await TestUtils.initIsar(); + TestUtils.init(); }); setUp(() async { - mapState = MapState(themeMode: ThemeMode.dark); + mapState = const MapState(themeMode: ThemeMode.dark); mapStateNotifier = MockMapStateNotifier(mapState); await StoreService.init(storeRepository: IsarStoreRepository(db)); overrides = [ @@ -37,8 +38,7 @@ void main() { ]; }); - testWidgets("Return dark theme style when theme mode is dark", - (tester) async { + testWidgets("Return dark theme style when theme mode is dark", (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( MapThemeOverride( @@ -50,8 +50,7 @@ void main() { overrides: overrides, ); - mapStateNotifier.state = - mapState.copyWith(darkStyleFetched: const AsyncData("dark")); + mapStateNotifier.state = mapState.copyWith(darkStyleFetched: const AsyncData("dark")); await tester.pumpAndSettle(); expect(mapStyle?.valueOrNull, "dark"); }); @@ -75,8 +74,7 @@ void main() { expect(mapStyle?.hasError, isTrue); }); - testWidgets("Return light theme style when theme mode is light", - (tester) async { + testWidgets("Return light theme style when theme mode is light", (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( MapThemeOverride( @@ -109,8 +107,7 @@ void main() { overrides: overrides, ); - tester.binding.platformDispatcher.platformBrightnessTestValue = - Brightness.dark; + tester.binding.platformDispatcher.platformBrightnessTestValue = Brightness.dark; mapStateNotifier.state = mapState.copyWith( themeMode: ThemeMode.system, darkStyleFetched: const AsyncData("dark"), @@ -120,8 +117,7 @@ void main() { expect(mapStyle?.valueOrNull, "dark"); }); - testWidgets("Return light theme style when system is light", - (tester) async { + testWidgets("Return light theme style when system is light", (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( MapThemeOverride( @@ -133,8 +129,7 @@ void main() { overrides: overrides, ); - tester.binding.platformDispatcher.platformBrightnessTestValue = - Brightness.light; + tester.binding.platformDispatcher.platformBrightnessTestValue = Brightness.light; mapStateNotifier.state = mapState.copyWith( themeMode: ThemeMode.system, lightStyleFetched: const AsyncData("light"), @@ -144,8 +139,7 @@ void main() { expect(mapStyle?.valueOrNull, "light"); }); - testWidgets("Switches style when system brightness changes", - (tester) async { + testWidgets("Switches style when system brightness changes", (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( MapThemeOverride( @@ -157,8 +151,7 @@ void main() { overrides: overrides, ); - tester.binding.platformDispatcher.platformBrightnessTestValue = - Brightness.light; + tester.binding.platformDispatcher.platformBrightnessTestValue = Brightness.light; mapStateNotifier.state = mapState.copyWith( themeMode: ThemeMode.system, lightStyleFetched: const AsyncData("light"), @@ -167,8 +160,7 @@ void main() { await tester.pumpAndSettle(); expect(mapStyle?.valueOrNull, "light"); - tester.binding.platformDispatcher.platformBrightnessTestValue = - Brightness.dark; + tester.binding.platformDispatcher.platformBrightnessTestValue = Brightness.dark; await tester.pumpAndSettle(); expect(mapStyle?.valueOrNull, "dark"); }); diff --git a/mobile/test/modules/shared/shared_mocks.dart b/mobile/test/modules/shared/shared_mocks.dart index f50fde7040..790bbbd815 100644 --- a/mobile/test/modules/shared/shared_mocks.dart +++ b/mobile/test/modules/shared/shared_mocks.dart @@ -3,9 +3,7 @@ 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 { +class MockCurrentUserProvider extends StateNotifier with Mock implements CurrentUserProvider { MockCurrentUserProvider() : super(null); @override diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index f2da3ff4fc..2858e6a9e7 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -10,8 +10,8 @@ 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/store.repository.dart'; -import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -57,14 +57,11 @@ void main() { final MockExifInfoRepository exifInfoRepository = MockExifInfoRepository(); final MockIsarUserRepository userRepository = MockIsarUserRepository(); final MockETagRepository eTagRepository = MockETagRepository(); - final MockAlbumMediaRepository albumMediaRepository = - MockAlbumMediaRepository(); + final MockAlbumMediaRepository albumMediaRepository = MockAlbumMediaRepository(); final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository(); final MockAppSettingService appSettingService = MockAppSettingService(); - final MockLocalFilesManagerRepository localFilesManagerRepository = - MockLocalFilesManagerRepository(); - final MockPartnerApiRepository partnerApiRepository = - MockPartnerApiRepository(); + final MockLocalFilesManagerRepository localFilesManagerRepository = MockLocalFilesManagerRepository(); + final MockPartnerApiRepository partnerApiRepository = MockPartnerApiRepository(); final MockUserApiRepository userApiRepository = MockUserApiRepository(); final MockPartnerRepository partnerRepository = MockPartnerRepository(); final MockUserService userService = MockUserService(); @@ -115,13 +112,11 @@ void main() { userApiRepository, ); when(() => userService.getMyUser()).thenReturn(owner); - when(() => eTagRepository.get(owner.id)) - .thenAnswer((_) async => ETag(id: owner.id, time: DateTime.now())); + 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(sortBy: SortUserBy.id)).thenAnswer((_) async => [owner]); when(() => userRepository.getAll()).thenAnswer((_) async => [owner]); when( () => assetRepository.getAll( @@ -133,8 +128,7 @@ void main() { .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(() => exifInfoRepository.updateAll(any())).thenAnswer((_) async => []); when(() => assetRepository.transaction(any())).thenAnswer( (call) => (call.positionalArguments.first as Function).call(), ); @@ -143,8 +137,7 @@ void main() { ); when(() => userApiRepository.getAll()).thenAnswer((_) async => [owner]); registerFallbackValue(Direction.sharedByMe); - when(() => partnerApiRepository.getAll(any())) - .thenAnswer((_) async => []); + when(() => partnerApiRepository.getAll(any())).thenAnswer((_) async => []); }); test('test inserting existing assets', () async { final List remoteAssets = [ @@ -178,8 +171,7 @@ void main() { expect(c1, isTrue); final updatedAsset = initialAssets[3].updatedCopy(remoteAssets[3]); verify( - () => assetRepository - .updateAll([remoteAssets[4], remoteAssets[5], updatedAsset]), + () => assetRepository.updateAll([remoteAssets[4], remoteAssets[5], updatedAsset]), ); }); @@ -240,10 +232,11 @@ void main() { [initialAssets[1].remoteId!, initialAssets[2].remoteId!], state: AssetState.remote, ), - ).thenAnswer((_) async {}); + ).thenAnswer((_) async { + return; + }); when( - () => assetRepository - .getAllByRemoteId(["2-1", "1-1"], state: AssetState.merged), + () => 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 diff --git a/mobile/test/modules/utils/throttler_test.dart b/mobile/test/modules/utils/throttler_test.dart index 76d8bd2ad7..ba5d542fe6 100644 --- a/mobile/test/modules/utils/throttler_test.dart +++ b/mobile/test/modules/utils/throttler_test.dart @@ -14,8 +14,7 @@ class _Counter { } void main() { - test('Executes the method immediately if no calls received previously', - () async { + 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()); diff --git a/mobile/test/modules/utils/url_helper_test.dart b/mobile/test/modules/utils/url_helper_test.dart index 840ac91f1f..0e8a8e2aa0 100644 --- a/mobile/test/modules/utils/url_helper_test.dart +++ b/mobile/test/modules/utils/url_helper_test.dart @@ -28,9 +28,7 @@ void main() { expect(punycodeEncodeUrl(url), equals(expected)); }); - test( - 'should encode multi-segment Unicode host with multiple non-ASCII segments', - () { + test('should encode multi-segment Unicode host with multiple non-ASCII segments', () { const url = 'https://bÃŧcher.mÃŧnchen'; const expected = 'https://xn--bcher-kva.xn--mnchen-3ya'; expect(punycodeEncodeUrl(url), equals(expected)); diff --git a/mobile/test/pages/search/search.page_test.dart b/mobile/test/pages/search/search.page_test.dart index fa7f037da5..1c1a410150 100644 --- a/mobile/test/pages/search/search.page_test.dart +++ b/mobile/test/pages/search/search.page_test.dart @@ -42,8 +42,7 @@ void main() { ]; }); - final emptyTextSearch = isA() - .having((s) => s.originalFileName, 'originalFileName', null); + final emptyTextSearch = isA().having((s) => s.originalFileName, 'originalFileName', null); testWidgets('contextual search with/without text', (tester) async { await tester.pumpConsumerWidget( @@ -111,8 +110,7 @@ void main() { expect( captured.first, - isA() - .having((s) => s.originalFileName, 'originalFileName', 'test'), + isA().having((s) => s.originalFileName, 'originalFileName', 'test'), ); await tester.enterText(searchField, ''); diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index 638b08c1ea..4b54ec4055 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -1,50 +1,48 @@ -import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; -import 'package:immich_mobile/interfaces/album.interface.dart'; -import 'package:immich_mobile/interfaces/asset.interface.dart'; -import 'package:immich_mobile/interfaces/asset_api.interface.dart'; -import 'package:immich_mobile/interfaces/asset_media.interface.dart'; -import 'package:immich_mobile/interfaces/auth.interface.dart'; -import 'package:immich_mobile/interfaces/auth_api.interface.dart'; -import 'package:immich_mobile/interfaces/backup_album.interface.dart'; -import 'package:immich_mobile/interfaces/etag.interface.dart'; -import 'package:immich_mobile/interfaces/file_media.interface.dart'; -import 'package:immich_mobile/interfaces/local_files_manager.interface.dart'; +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/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:mocktail/mocktail.dart'; -class MockAlbumRepository extends Mock implements IAlbumRepository {} +class MockAlbumRepository extends Mock implements AlbumRepository {} -class MockAssetRepository extends Mock implements IAssetRepository {} +class MockAssetRepository extends Mock implements AssetRepository {} -class MockBackupRepository extends Mock implements IBackupAlbumRepository {} +class MockBackupRepository extends Mock implements BackupAlbumRepository {} -class MockExifInfoRepository extends Mock implements IExifInfoRepository {} +class MockExifInfoRepository extends Mock implements IsarExifRepository {} -class MockETagRepository extends Mock implements IETagRepository {} +class MockETagRepository extends Mock implements ETagRepository {} class MockAlbumMediaRepository extends Mock implements AlbumMediaRepository {} -class MockBackupAlbumRepository extends Mock - implements IBackupAlbumRepository {} +class MockBackupAlbumRepository extends Mock implements BackupAlbumRepository {} -class MockAssetApiRepository extends Mock implements IAssetApiRepository {} +class MockAssetApiRepository extends Mock implements AssetApiRepository {} -class MockAssetMediaRepository extends Mock implements IAssetMediaRepository {} +class MockAssetMediaRepository extends Mock implements AssetMediaRepository {} -class MockFileMediaRepository extends Mock implements IFileMediaRepository {} +class MockFileMediaRepository extends Mock implements FileMediaRepository {} class MockAlbumApiRepository extends Mock implements AlbumApiRepository {} -class MockAuthApiRepository extends Mock implements IAuthApiRepository {} +class MockAuthApiRepository extends Mock implements AuthApiRepository {} -class MockAuthRepository extends Mock implements IAuthRepository {} +class MockAuthRepository extends Mock implements AuthRepository {} class MockPartnerRepository extends Mock implements PartnerRepository {} class MockPartnerApiRepository extends Mock implements PartnerApiRepository {} -class MockLocalFilesManagerRepository extends Mock - implements ILocalFilesManager {} +class MockLocalFilesManagerRepository extends Mock implements LocalFilesManagerRepository {} diff --git a/mobile/test/services/album.service_test.dart b/mobile/test/services/album.service_test.dart index 443e37e75d..547b049593 100644 --- a/mobile/test/services/album.service_test.dart +++ b/mobile/test/services/album.service_test.dart @@ -54,28 +54,22 @@ void main() { 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(() => 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); + 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.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); + 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( @@ -88,10 +82,8 @@ void main() { 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(() => 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]); @@ -142,8 +134,7 @@ void main() { () => albumRepository.create(AlbumStub.oneAsset), ).thenAnswer((_) async => AlbumStub.twoAsset); - final result = - await sut.createAlbum("name", [AssetStub.image1], [UserStub.user1]); + final result = await sut.createAlbum("name", [AssetStub.image1], [UserStub.user1]); expect(result, AlbumStub.twoAsset); verify( () => albumApiRepository.create( @@ -163,10 +154,7 @@ void main() { when( () => albumApiRepository.addAssets(AlbumStub.oneAsset.remoteId!, any()), ).thenAnswer( - (_) async => ( - added: [AssetStub.image2.remoteId!], - duplicates: [AssetStub.image1.remoteId!] - ), + (_) async => (added: [AssetStub.image2.remoteId!], duplicates: [AssetStub.image1.remoteId!]), ); when( () => albumRepository.get(AlbumStub.oneAsset.id), @@ -198,8 +186,7 @@ void main() { group('addAdditionalUserToAlbum', () { test('one added', () async { when( - () => - albumApiRepository.addUsers(AlbumStub.emptyAlbum.remoteId!, any()), + () => albumApiRepository.addUsers(AlbumStub.emptyAlbum.remoteId!, any()), ).thenAnswer( (_) async => AlbumStub.sharedWithUser, ); diff --git a/mobile/test/services/asset.service_test.dart b/mobile/test/services/asset.service_test.dart index 293e5ec76b..5077764f26 100644 --- a/mobile/test/services/asset.service_test.dart +++ b/mobile/test/services/asset.service_test.dart @@ -69,8 +69,7 @@ void main() { setUp(() { assetsApi = MockAssetsApi(); when(() => apiService.assetsApi).thenReturn(assetsApi); - when(() => assetsApi.updateAssets(any())) - .thenAnswer((_) async => Future.value()); + when(() => assetsApi.updateAssets(any())).thenAnswer((_) async => Future.value()); }); test("asset is updated with DateTime", () async { @@ -79,11 +78,9 @@ void main() { await sut.changeDateTime(assets, dateTime.toIso8601String()); verify(() => assetsApi.updateAssets(any())).called(1); - final upsertExifCallback = - verify(() => syncService.upsertAssetsWithExif(captureAny())); + final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny())); upsertExifCallback.called(1); - final receivedAssets = - upsertExifCallback.captured.firstOrNull as List? ?? []; + final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; final receivedDatetime = receivedAssets.cast().map( (a) => a.exifInfo?.dateTimeOriginal ?? DateTime(0), ); @@ -96,14 +93,11 @@ void main() { await sut.changeLocation(assets, latLng); verify(() => assetsApi.updateAssets(any())).called(1); - final upsertExifCallback = - verify(() => syncService.upsertAssetsWithExif(captureAny())); + final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny())); upsertExifCallback.called(1); - final receivedAssets = - upsertExifCallback.captured.firstOrNull as List? ?? []; + final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; final receivedCoords = receivedAssets.cast().map( - (a) => - LatLng(a.exifInfo?.latitude ?? 0, a.exifInfo?.longitude ?? 0), + (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 4ada98a6c9..c9b44fe28b 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/services/store.service.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'; @@ -20,6 +21,8 @@ void main() { late MockApiService apiService; late MockNetworkService networkService; late MockBackgroundSyncManager backgroundSyncManager; + late MockUploadService uploadService; + late MockAppSettingService appSettingsService; late Isar db; setUp(() async { @@ -28,6 +31,8 @@ void main() { apiService = MockApiService(); networkService = MockNetworkService(); backgroundSyncManager = MockBackgroundSyncManager(); + uploadService = MockUploadService(); + appSettingsService = MockAppSettingService(); sut = AuthService( authApiRepository, @@ -35,6 +40,7 @@ void main() { apiService, networkService, backgroundSyncManager, + appSettingsService, ); registerFallbackValue(Uri()); @@ -58,8 +64,7 @@ void main() { const testUrl = 'http://ip:2283'; const resolvedUrl = 'http://ip:2283/api'; - when(() => apiService.resolveAndSetEndpoint(testUrl)) - .thenAnswer((_) async => resolvedUrl); + when(() => apiService.resolveAndSetEndpoint(testUrl)).thenAnswer((_) async => resolvedUrl); when(() => apiService.setDeviceInfoHeader()).thenAnswer((_) async => {}); final result = await sut.validateServerUrl(testUrl); @@ -74,8 +79,7 @@ void main() { const testUrl = 'https://immich.domain.com'; const resolvedUrl = 'https://immich.domain.com/api'; - when(() => apiService.resolveAndSetEndpoint(testUrl)) - .thenAnswer((_) async => resolvedUrl); + when(() => apiService.resolveAndSetEndpoint(testUrl)).thenAnswer((_) async => resolvedUrl); when(() => apiService.setDeviceInfoHeader()).thenAnswer((_) async => {}); final result = await sut.validateServerUrl(testUrl); @@ -89,8 +93,7 @@ void main() { test('Should throw error on invalid URL', () async { const testUrl = 'invalid-url'; - when(() => apiService.resolveAndSetEndpoint(testUrl)) - .thenThrow(Exception('Invalid URL')); + when(() => apiService.resolveAndSetEndpoint(testUrl)).thenThrow(Exception('Invalid URL')); expect( () async => await sut.validateServerUrl(testUrl), @@ -104,8 +107,7 @@ void main() { test('Should throw error on unreachable server', () async { const testUrl = 'https://unreachable.server'; - when(() => apiService.resolveAndSetEndpoint(testUrl)) - .thenThrow(Exception('Server is not reachable')); + when(() => apiService.resolveAndSetEndpoint(testUrl)).thenThrow(Exception('Server is not reachable')); expect( () async => await sut.validateServerUrl(testUrl), @@ -121,9 +123,14 @@ void main() { test('Should logout user', () async { when(() => authApiRepository.logout()).thenAnswer((_) async => {}); when(() => backgroundSyncManager.cancel()).thenAnswer((_) async => {}); - when(() => authRepository.clearLocalData()) - .thenAnswer((_) => Future.value(null)); - + when(() => authRepository.clearLocalData()).thenAnswer((_) => Future.value(null)); + when(() => uploadService.cancelBackup()).thenAnswer((_) => Future.value(1)); + when( + () => appSettingsService.setSetting( + AppSettingsEnum.enableBackup, + false, + ), + ).thenAnswer((_) => Future.value(null)); await sut.logout(); verify(() => authApiRepository.logout()).called(1); @@ -132,12 +139,16 @@ void main() { }); test('Should clear local data even on server error', () async { - when(() => authApiRepository.logout()) - .thenThrow(Exception('Server error')); + when(() => authApiRepository.logout()).thenThrow(Exception('Server error')); when(() => backgroundSyncManager.cancel()).thenAnswer((_) async => {}); - when(() => authRepository.clearLocalData()) - .thenAnswer((_) => Future.value(null)); - + when(() => authRepository.clearLocalData()).thenAnswer((_) => Future.value(null)); + when(() => uploadService.cancelBackup()).thenAnswer((_) => Future.value(1)); + when( + () => appSettingsService.setSetting( + AppSettingsEnum.enableBackup, + false, + ), + ).thenAnswer((_) => Future.value(null)); await sut.logout(); verify(() => authApiRepository.logout()).called(1); @@ -148,13 +159,11 @@ void main() { group('setOpenApiServiceEndpoint', () { setUp(() { - when(() => networkService.getWifiName()) - .thenAnswer((_) async => 'TestWifi'); + when(() => networkService.getWifiName()).thenAnswer((_) async => 'TestWifi'); }); test('Should return null if auto endpoint switching is disabled', () async { - when(() => authRepository.getEndpointSwitchingFeature()) - .thenReturn((false)); + when(() => authRepository.getEndpointSwitchingFeature()).thenReturn((false)); final result = await sut.setOpenApiServiceEndpoint(); @@ -166,8 +175,7 @@ void main() { test('Should set local connection if wifi name matches', () async { when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); when(() => authRepository.getPreferredWifiName()).thenReturn('TestWifi'); - when(() => authRepository.getLocalEndpoint()) - .thenReturn('http://local.endpoint'); + when(() => authRepository.getLocalEndpoint()).thenReturn('http://local.endpoint'); when(() => apiService.resolveAndSetEndpoint('http://local.endpoint')) .thenAnswer((_) async => 'http://local.endpoint'); @@ -178,16 +186,14 @@ void main() { verify(() => networkService.getWifiName()).called(1); verify(() => authRepository.getPreferredWifiName()).called(1); verify(() => authRepository.getLocalEndpoint()).called(1); - verify(() => apiService.resolveAndSetEndpoint('http://local.endpoint')) - .called(1); + verify(() => apiService.resolveAndSetEndpoint('http://local.endpoint')).called(1); }); test('Should set external endpoint if wifi name not matching', () async { when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); - when(() => authRepository.getPreferredWifiName()) - .thenReturn('DifferentWifi'); + when(() => authRepository.getPreferredWifiName()).thenReturn('DifferentWifi'); when(() => authRepository.getExternalEndpointList()).thenReturn([ - AuxilaryEndpoint( + const AuxilaryEndpoint( url: 'https://external.endpoint', status: AuxCheckStatus.valid, ), @@ -208,17 +214,15 @@ void main() { ).called(1); }); - test('Should set second external endpoint if the first throw any error', - () async { + test('Should set second external endpoint if the first throw any error', () async { when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); - when(() => authRepository.getPreferredWifiName()) - .thenReturn('DifferentWifi'); + when(() => authRepository.getPreferredWifiName()).thenReturn('DifferentWifi'); when(() => authRepository.getExternalEndpointList()).thenReturn([ - AuxilaryEndpoint( + const AuxilaryEndpoint( url: 'https://external.endpoint', status: AuxCheckStatus.valid, ), - AuxilaryEndpoint( + const AuxilaryEndpoint( url: 'https://external.endpoint2', status: AuxCheckStatus.valid, ), @@ -243,17 +247,15 @@ void main() { ).called(1); }); - test('Should set second external endpoint if the first throw ApiException', - () async { + test('Should set second external endpoint if the first throw ApiException', () async { when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); - when(() => authRepository.getPreferredWifiName()) - .thenReturn('DifferentWifi'); + when(() => authRepository.getPreferredWifiName()).thenReturn('DifferentWifi'); when(() => authRepository.getExternalEndpointList()).thenReturn([ - AuxilaryEndpoint( + const AuxilaryEndpoint( url: 'https://external.endpoint', status: AuxCheckStatus.valid, ), - AuxilaryEndpoint( + const AuxilaryEndpoint( url: 'https://external.endpoint2', status: AuxCheckStatus.valid, ), @@ -281,8 +283,7 @@ void main() { test('Should handle error when setting local connection', () async { when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); when(() => authRepository.getPreferredWifiName()).thenReturn('TestWifi'); - when(() => authRepository.getLocalEndpoint()) - .thenReturn('http://local.endpoint'); + when(() => authRepository.getLocalEndpoint()).thenReturn('http://local.endpoint'); when(() => apiService.resolveAndSetEndpoint('http://local.endpoint')) .thenThrow(Exception('Local endpoint error')); @@ -293,16 +294,14 @@ void main() { verify(() => networkService.getWifiName()).called(1); verify(() => authRepository.getPreferredWifiName()).called(1); verify(() => authRepository.getLocalEndpoint()).called(1); - verify(() => apiService.resolveAndSetEndpoint('http://local.endpoint')) - .called(1); + verify(() => apiService.resolveAndSetEndpoint('http://local.endpoint')).called(1); }); test('Should handle error when setting external connection', () async { when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); - when(() => authRepository.getPreferredWifiName()) - .thenReturn('DifferentWifi'); + when(() => authRepository.getPreferredWifiName()).thenReturn('DifferentWifi'); when(() => authRepository.getExternalEndpointList()).thenReturn([ - AuxilaryEndpoint( + const AuxilaryEndpoint( url: 'https://external.endpoint', status: AuxCheckStatus.valid, ), diff --git a/mobile/test/services/entity.service_test.dart b/mobile/test/services/entity.service_test.dart index 1642be7fb3..7aab7f9428 100644 --- a/mobile/test/services/entity.service_test.dart +++ b/mobile/test/services/entity.service_test.dart @@ -21,8 +21,7 @@ void main() { }); group('fillAlbumWithDatabaseEntities', () { - test('remote album with owner, thumbnail, sharedUsers and assets', - () async { + 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", @@ -41,19 +40,14 @@ void main() { [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(() => 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(() => assetRepository.getByRemoteId(AssetStub.image1.remoteId!)).thenAnswer((_) async => AssetStub.image1); - when(() => userRepository.getByUserIds(any())) - .thenAnswer((_) async => [UserStub.user1, UserStub.user2]); + when(() => userRepository.getByUserIds(any())).thenAnswer((_) async => [UserStub.user1, UserStub.user2]); - when(() => assetRepository.getAllByRemoteId(any())) - .thenAnswer((_) async => [AssetStub.image1, AssetStub.image2]); + when(() => assetRepository.getAllByRemoteId(any())).thenAnswer((_) async => [AssetStub.image1, AssetStub.image2]); await sut.fillAlbumWithDatabaseEntities(album); expect(album.owner.value?.toDto(), UserStub.admin); diff --git a/mobile/test/services/hash_service_test.dart b/mobile/test/services/hash_service_test.dart index e278199e4f..5360e30341 100644 --- a/mobile/test/services/hash_service_test.dart +++ b/mobile/test/services/hash_service_test.dart @@ -6,9 +6,9 @@ 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/interfaces/device_asset.interface.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'; @@ -25,7 +25,7 @@ class MockAssetEntity extends Mock implements AssetEntity {} void main() { late HashService sut; late BackgroundService mockBackgroundService; - late IDeviceAssetRepository mockDeviceAssetRepository; + late IsarDeviceAssetRepository mockDeviceAssetRepository; setUp(() { mockBackgroundService = MockBackgroundService(); @@ -36,27 +36,22 @@ void main() { backgroundService: mockBackgroundService, ); - when(() => mockDeviceAssetRepository.transaction(any())) - .thenAnswer((_) 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(); }); - when(() => mockDeviceAssetRepository.updateAll(any())) - .thenAnswer((_) async => true); - when(() => mockDeviceAssetRepository.deleteIds(any())) - .thenAnswer((_) async => true); + 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); + final (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - when(() => mockBackgroundService.digestFiles([file.path])) - .thenAnswer((_) async => [hash]); + when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); // No DB entries for this asset when( () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]), @@ -65,14 +60,12 @@ void main() { final result = await sut.hashAssets([mockAsset]); // Verify we stored the new hash in DB - when(() => mockDeviceAssetRepository.transaction(any())) - .thenAnswer((_) 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(); + await (capturedCallback.firstOrNull as Future Function()?)?.call(); verify( () => mockDeviceAssetRepository.updateAll([ deviceAsset.copyWith(modifiedTime: AssetStub.image1.fileModifiedAt), @@ -115,25 +108,21 @@ void main() { }); test("hashed successful when asset is modified", () async { - final (mockAsset, file, deviceAsset, hash) = - await _createAssetMock(AssetStub.image1); + final (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - when(() => mockBackgroundService.digestFiles([file.path])) - .thenAnswer((_) async => [hash]); + 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 { + when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { final capturedCallback = verify( () => mockDeviceAssetRepository.transaction(captureAny()), ).captured; // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?) - ?.call(); + await (capturedCallback.firstOrNull as Future Function()?)?.call(); verify( () => mockDeviceAssetRepository.updateAll([ deviceAsset.copyWith(modifiedTime: AssetStub.image1.fileModifiedAt), @@ -157,11 +146,9 @@ void main() { late File file; setUp(() async { - (mockAsset, file, deviceAsset, hash) = - await _createAssetMock(AssetStub.image1); + (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - when(() => mockBackgroundService.digestFiles([file.path])) - .thenAnswer((_) async => [hash]); + when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); when( () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]), ).thenAnswer((_) async => [deviceAsset]); @@ -182,14 +169,12 @@ void main() { }); test("cleanups DeviceAsset when hashing failed", () async { - when(() => mockDeviceAssetRepository.transaction(any())) - .thenAnswer((_) 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(); + 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 @@ -210,8 +195,7 @@ void main() { // and verify the results inside the transaction stub verify(() => mockDeviceAssetRepository.updateAll([])).called(1); verify( - () => - mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!]), + () => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!]), ).called(1); }); @@ -240,14 +224,11 @@ void main() { final (asset2, file2, deviceAsset2, hash2) = mock2; final (asset3, file3, deviceAsset3, hash3) = mock3; - when(() => mockDeviceAssetRepository.getByIds(any())) - .thenAnswer((_) async => []); + 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]); + 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(); @@ -259,8 +240,7 @@ void main() { 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([file1.path, file2.path])).called(1); verify(() => mockBackgroundService.digestFiles([file3.path])).called(1); expect( @@ -285,15 +265,11 @@ void main() { final (asset2, file2, deviceAsset2, hash2) = mock2; final (asset3, file3, deviceAsset3, hash3) = mock3; - when(() => mockDeviceAssetRepository.getByIds(any())) - .thenAnswer((_) async => []); + 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]); + 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, @@ -318,17 +294,12 @@ void main() { }); 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 + 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]); + when(() => mockBackgroundService.digestFiles([file1.path, file3.path])).thenAnswer((_) async => [hash1, hash3]); // DB entries are not sorted and a dummy entry added when( () => mockDeviceAssetRepository.getByIds([ @@ -349,8 +320,7 @@ void main() { final result = await sut.hashAssets([asset1, asset2, asset3, asset4]); // Verify correct processing of all assets - verify(() => mockBackgroundService.digestFiles([file1.path, file3.path])) - .called(1); + verify(() => mockBackgroundService.digestFiles([file1.path, file3.path])).called(1); expect(result.length, 3); expect(result, [ AssetStub.image2.copyWith(checksum: base64.encode(hash2)), @@ -361,8 +331,7 @@ void main() { group("HashService: Edge cases", () { test("handles empty list of assets", () async { - when(() => mockDeviceAssetRepository.getByIds(any())) - .thenAnswer((_) async => []); + when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); final result = await sut.hashAssets([]); @@ -398,8 +367,7 @@ Future<(Asset, File, DeviceAsset, Uint8List)> _createAssetMock( Asset asset, ) async { final random = Random(); - final hash = - Uint8List.fromList(List.generate(20, (i) => random.nextInt(255))); + final hash = Uint8List.fromList(List.generate(20, (i) => random.nextInt(255))); final mockAsset = MockAsset(); final mockAssetEntity = MockAssetEntity(); final fs = MemoryFileSystem(); diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index c0f789795c..596d3bcd1c 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -24,7 +24,6 @@ import 'mock_http_override.dart'; // Listener Mock to test when a provider notifies its listeners class ListenerMock extends Mock { - // ignore: avoid-declaring-call-method void call(T? previous, T next); } diff --git a/mobile/test/test_utils/medium_factory.dart b/mobile/test/test_utils/medium_factory.dart index 64c23321aa..19ad7166c6 100644 --- a/mobile/test/test_utils/medium_factory.dart +++ b/mobile/test/test_utils/medium_factory.dart @@ -1,8 +1,7 @@ import 'dart:math'; -import 'package:immich_mobile/domain/interfaces/local_album.interface.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/local_album.model.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; @@ -26,10 +25,8 @@ class MediumFactory { 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)), + createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), + updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), ); } @@ -46,8 +43,7 @@ class MediumFactory { return LocalAlbum( id: id ?? '${random.nextInt(1000000)}', name: name ?? 'Album ${random.nextInt(1000000)}', - updatedAt: updatedAt ?? - DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), + updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), assetCount: assetCount ?? random.nextInt(100), backupSelection: backupSelection ?? BackupSelection.none, isIosSharedAlbum: isIosSharedAlbum ?? false, @@ -56,7 +52,7 @@ class MediumFactory { T getRepository() { switch (T) { - case const (ILocalAlbumRepository): + case const (DriftLocalAlbumRepository): return DriftLocalAlbumRepository(_db) as T; default: throw Exception('Unknown repository: $T'); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index bd5e8e7fa0..576592413d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4546,6 +4546,39 @@ } }, "/people": { + "delete": { + "operationId": "deletePeople", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkIdsDto" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "People" + ] + }, "get": { "operationId": "getAllPeople", "parameters": [ @@ -4711,6 +4744,39 @@ } }, "/people/{id}": { + "delete": { + "operationId": "deletePerson", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "People" + ] + }, "get": { "operationId": "getPerson", "parameters": [ @@ -5958,6 +6024,56 @@ "tags": [ "Sessions" ] + }, + "put": { + "operationId": "updateSession", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Sessions" + ] } }, "/sessions/{id}/lock": { @@ -6630,6 +6746,50 @@ ] } }, + "/stacks/{id}/assets/{assetId}": { + "delete": { + "operationId": "removeAssetFromStack", + "parameters": [ + { + "name": "assetId", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Stacks" + ] + } + }, "/sync/ack": { "delete": { "operationId": "deleteSyncAck", @@ -8503,7 +8663,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.135.3", + "version": "1.136.0", "contact": {} }, "tags": [], @@ -11137,6 +11297,7 @@ "format": "uuid", "type": "string" }, + "nullable": true, "type": "array" }, "takenAfter": { @@ -11599,25 +11760,35 @@ "asset.read", "asset.update", "asset.delete", + "asset.statistics", "asset.share", "asset.view", "asset.download", "asset.upload", + "asset.replace", "album.create", "album.read", "album.update", "album.delete", "album.statistics", - "album.addAsset", - "album.removeAsset", "album.share", "album.download", + "albumAsset.create", + "albumAsset.delete", + "albumUser.create", + "albumUser.update", + "albumUser.delete", + "auth.changePassword", "authDevice.delete", "archive.read", + "duplicate.read", + "duplicate.delete", "face.create", "face.read", "face.update", "face.delete", + "job.create", + "job.read", "library.create", "library.read", "library.update", @@ -11629,6 +11800,9 @@ "memory.read", "memory.update", "memory.delete", + "memory.statistics", + "memoryAsset.create", + "memoryAsset.delete", "notification.create", "notification.read", "notification.update", @@ -11644,6 +11818,16 @@ "person.statistics", "person.merge", "person.reassign", + "pinCode.create", + "pinCode.update", + "pinCode.delete", + "server.about", + "server.apkLinks", + "server.storage", + "server.statistics", + "serverLicense.read", + "serverLicense.update", + "serverLicense.delete", "session.create", "session.read", "session.update", @@ -11657,6 +11841,10 @@ "stack.read", "stack.update", "stack.delete", + "sync.stream", + "syncCheckpoint.read", + "syncCheckpoint.update", + "syncCheckpoint.delete", "systemConfig.read", "systemConfig.update", "systemMetadata.read", @@ -11666,10 +11854,25 @@ "tag.update", "tag.delete", "tag.asset", - "admin.user.create", - "admin.user.read", - "admin.user.update", - "admin.user.delete" + "user.read", + "user.update", + "userLicense.create", + "userLicense.read", + "userLicense.update", + "userLicense.delete", + "userOnboarding.read", + "userOnboarding.update", + "userOnboarding.delete", + "userPreference.read", + "userPreference.update", + "userProfileImage.create", + "userProfileImage.read", + "userProfileImage.update", + "userProfileImage.delete", + "adminUser.create", + "adminUser.read", + "adminUser.update", + "adminUser.delete" ], "type": "string" }, @@ -12026,6 +12229,7 @@ "format": "uuid", "type": "string" }, + "nullable": true, "type": "array" }, "takenAfter": { @@ -12722,6 +12926,9 @@ "id": { "type": "string" }, + "isPendingSyncReset": { + "type": "boolean" + }, "token": { "type": "string" }, @@ -12735,6 +12942,7 @@ "deviceOS", "deviceType", "id", + "isPendingSyncReset", "token", "updatedAt" ], @@ -12760,6 +12968,9 @@ "id": { "type": "string" }, + "isPendingSyncReset": { + "type": "boolean" + }, "updatedAt": { "type": "string" } @@ -12770,6 +12981,7 @@ "deviceOS", "deviceType", "id", + "isPendingSyncReset", "updatedAt" ], "type": "object" @@ -12786,6 +12998,14 @@ }, "type": "object" }, + "SessionUpdateDto": { + "properties": { + "isPendingSyncReset": { + "type": "boolean" + } + }, + "type": "object" + }, "SharedLinkCreateDto": { "properties": { "albumId": { @@ -13091,6 +13311,7 @@ "format": "uuid", "type": "string" }, + "nullable": true, "type": "array" }, "takenAfter": { @@ -13282,6 +13503,7 @@ "format": "uuid", "type": "string" }, + "nullable": true, "type": "array" }, "takenAfter": { @@ -13361,6 +13583,7 @@ "items": { "type": "string" }, + "maxItems": 1000, "type": "array" } }, @@ -13369,6 +13592,10 @@ ], "type": "object" }, + "SyncAckV1": { + "properties": {}, + "type": "object" + }, "SyncAlbumDeleteV1": { "properties": { "albumId": { @@ -13380,6 +13607,36 @@ ], "type": "object" }, + "SyncAlbumToAssetDeleteV1": { + "properties": { + "albumId": { + "type": "string" + }, + "assetId": { + "type": "string" + } + }, + "required": [ + "albumId", + "assetId" + ], + "type": "object" + }, + "SyncAlbumToAssetV1": { + "properties": { + "albumId": { + "type": "string" + }, + "assetId": { + "type": "string" + } + }, + "required": [ + "albumId", + "assetId" + ], + "type": "object" + }, "SyncAlbumUserDeleteV1": { "properties": { "albumId": { @@ -13514,36 +13771,41 @@ "type": "string" }, "fNumber": { + "format": "double", "nullable": true, - "type": "integer" + "type": "number" }, "fileSizeInByte": { "nullable": true, "type": "integer" }, "focalLength": { + "format": "double", "nullable": true, - "type": "integer" + "type": "number" }, "fps": { + "format": "double", "nullable": true, - "type": "integer" + "type": "number" }, "iso": { "nullable": true, "type": "integer" }, "latitude": { + "format": "double", "nullable": true, - "type": "integer" + "type": "number" }, "lensModel": { "nullable": true, "type": "string" }, "longitude": { + "format": "double", "nullable": true, - "type": "integer" + "type": "number" }, "make": { "nullable": true, @@ -13612,6 +13874,65 @@ ], "type": "object" }, + "SyncAssetFaceDeleteV1": { + "properties": { + "assetFaceId": { + "type": "string" + } + }, + "required": [ + "assetFaceId" + ], + "type": "object" + }, + "SyncAssetFaceV1": { + "properties": { + "assetId": { + "type": "string" + }, + "boundingBoxX1": { + "type": "integer" + }, + "boundingBoxX2": { + "type": "integer" + }, + "boundingBoxY1": { + "type": "integer" + }, + "boundingBoxY2": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "imageHeight": { + "type": "integer" + }, + "imageWidth": { + "type": "integer" + }, + "personId": { + "nullable": true, + "type": "string" + }, + "sourceType": { + "type": "string" + } + }, + "required": [ + "assetId", + "boundingBoxX1", + "boundingBoxX2", + "boundingBoxY1", + "boundingBoxY2", + "id", + "imageHeight", + "imageWidth", + "personId", + "sourceType" + ], + "type": "object" + }, "SyncAssetV1": { "properties": { "checksum": { @@ -13642,6 +13963,10 @@ "isFavorite": { "type": "boolean" }, + "livePhotoVideoId": { + "nullable": true, + "type": "string" + }, "localDateTime": { "format": "date-time", "nullable": true, @@ -13653,6 +13978,10 @@ "ownerId": { "type": "string" }, + "stackId": { + "nullable": true, + "type": "string" + }, "thumbhash": { "nullable": true, "type": "string" @@ -13680,38 +14009,245 @@ "fileModifiedAt", "id", "isFavorite", + "livePhotoVideoId", "localDateTime", "originalFileName", "ownerId", + "stackId", "thumbhash", "type", "visibility" ], "type": "object" }, + "SyncAuthUserV1": { + "properties": { + "avatarColor": { + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ], + "nullable": true + }, + "deletedAt": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "email": { + "type": "string" + }, + "hasProfileImage": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "isAdmin": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "oauthId": { + "type": "string" + }, + "pinCode": { + "nullable": true, + "type": "string" + }, + "profileChangedAt": { + "format": "date-time", + "type": "string" + }, + "quotaSizeInBytes": { + "nullable": true, + "type": "integer" + }, + "quotaUsageInBytes": { + "type": "integer" + }, + "storageLabel": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "avatarColor", + "deletedAt", + "email", + "hasProfileImage", + "id", + "isAdmin", + "name", + "oauthId", + "pinCode", + "profileChangedAt", + "quotaSizeInBytes", + "quotaUsageInBytes", + "storageLabel" + ], + "type": "object" + }, "SyncEntityType": { "enum": [ + "AuthUserV1", "UserV1", "UserDeleteV1", - "PartnerV1", - "PartnerDeleteV1", "AssetV1", "AssetDeleteV1", "AssetExifV1", + "PartnerV1", + "PartnerDeleteV1", "PartnerAssetV1", "PartnerAssetBackfillV1", "PartnerAssetDeleteV1", "PartnerAssetExifV1", "PartnerAssetExifBackfillV1", + "PartnerStackBackfillV1", + "PartnerStackDeleteV1", + "PartnerStackV1", "AlbumV1", "AlbumDeleteV1", "AlbumUserV1", "AlbumUserBackfillV1", "AlbumUserDeleteV1", - "SyncAckV1" + "AlbumAssetV1", + "AlbumAssetBackfillV1", + "AlbumAssetExifV1", + "AlbumAssetExifBackfillV1", + "AlbumToAssetV1", + "AlbumToAssetDeleteV1", + "AlbumToAssetBackfillV1", + "MemoryV1", + "MemoryDeleteV1", + "MemoryToAssetV1", + "MemoryToAssetDeleteV1", + "StackV1", + "StackDeleteV1", + "PersonV1", + "PersonDeleteV1", + "AssetFaceV1", + "AssetFaceDeleteV1", + "UserMetadataV1", + "UserMetadataDeleteV1", + "SyncAckV1", + "SyncResetV1" ], "type": "string" }, + "SyncMemoryAssetDeleteV1": { + "properties": { + "assetId": { + "type": "string" + }, + "memoryId": { + "type": "string" + } + }, + "required": [ + "assetId", + "memoryId" + ], + "type": "object" + }, + "SyncMemoryAssetV1": { + "properties": { + "assetId": { + "type": "string" + }, + "memoryId": { + "type": "string" + } + }, + "required": [ + "assetId", + "memoryId" + ], + "type": "object" + }, + "SyncMemoryDeleteV1": { + "properties": { + "memoryId": { + "type": "string" + } + }, + "required": [ + "memoryId" + ], + "type": "object" + }, + "SyncMemoryV1": { + "properties": { + "createdAt": { + "format": "date-time", + "type": "string" + }, + "data": { + "type": "object" + }, + "deletedAt": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "hideAt": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "id": { + "type": "string" + }, + "isSaved": { + "type": "boolean" + }, + "memoryAt": { + "format": "date-time", + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "seenAt": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "showAt": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/MemoryType" + } + ] + }, + "updatedAt": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "createdAt", + "data", + "deletedAt", + "hideAt", + "id", + "isSaved", + "memoryAt", + "ownerId", + "seenAt", + "showAt", + "type", + "updatedAt" + ], + "type": "object" + }, "SyncPartnerDeleteV1": { "properties": { "sharedById": { @@ -13746,21 +14282,143 @@ ], "type": "object" }, + "SyncPersonDeleteV1": { + "properties": { + "personId": { + "type": "string" + } + }, + "required": [ + "personId" + ], + "type": "object" + }, + "SyncPersonV1": { + "properties": { + "birthDate": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "color": { + "nullable": true, + "type": "string" + }, + "createdAt": { + "format": "date-time", + "type": "string" + }, + "faceAssetId": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "string" + }, + "isFavorite": { + "type": "boolean" + }, + "isHidden": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "updatedAt": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "birthDate", + "color", + "createdAt", + "faceAssetId", + "id", + "isFavorite", + "isHidden", + "name", + "ownerId", + "updatedAt" + ], + "type": "object" + }, "SyncRequestType": { "enum": [ - "UsersV1", - "PartnersV1", + "AlbumsV1", + "AlbumUsersV1", + "AlbumToAssetsV1", + "AlbumAssetsV1", + "AlbumAssetExifsV1", "AssetsV1", "AssetExifsV1", + "AuthUsersV1", + "MemoriesV1", + "MemoryToAssetsV1", + "PartnersV1", "PartnerAssetsV1", "PartnerAssetExifsV1", - "AlbumsV1", - "AlbumUsersV1" + "PartnerStacksV1", + "StacksV1", + "UsersV1", + "PeopleV1", + "AssetFacesV1", + "UserMetadataV1" ], "type": "string" }, + "SyncResetV1": { + "properties": {}, + "type": "object" + }, + "SyncStackDeleteV1": { + "properties": { + "stackId": { + "type": "string" + } + }, + "required": [ + "stackId" + ], + "type": "object" + }, + "SyncStackV1": { + "properties": { + "createdAt": { + "format": "date-time", + "type": "string" + }, + "id": { + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "primaryAssetId": { + "type": "string" + }, + "updatedAt": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "createdAt", + "id", + "ownerId", + "primaryAssetId", + "updatedAt" + ], + "type": "object" + }, "SyncStreamDto": { "properties": { + "reset": { + "type": "boolean" + }, "types": { "items": { "$ref": "#/components/schemas/SyncRequestType" @@ -13784,8 +14442,58 @@ ], "type": "object" }, + "SyncUserMetadataDeleteV1": { + "properties": { + "key": { + "allOf": [ + { + "$ref": "#/components/schemas/UserMetadataKey" + } + ] + }, + "userId": { + "type": "string" + } + }, + "required": [ + "key", + "userId" + ], + "type": "object" + }, + "SyncUserMetadataV1": { + "properties": { + "key": { + "allOf": [ + { + "$ref": "#/components/schemas/UserMetadataKey" + } + ] + }, + "userId": { + "type": "string" + }, + "value": { + "type": "object" + } + }, + "required": [ + "key", + "userId", + "value" + ], + "type": "object" + }, "SyncUserV1": { "properties": { + "avatarColor": { + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ], + "nullable": true + }, "deletedAt": { "format": "date-time", "nullable": true, @@ -13802,6 +14510,7 @@ } }, "required": [ + "avatarColor", "deletedAt", "email", "id", @@ -13852,6 +14561,9 @@ "newVersionCheck": { "$ref": "#/components/schemas/SystemConfigNewVersionCheckDto" }, + "nightlyTasks": { + "$ref": "#/components/schemas/SystemConfigNightlyTasksDto" + }, "notifications": { "$ref": "#/components/schemas/SystemConfigNotificationsDto" }, @@ -13894,6 +14606,7 @@ "map", "metadata", "newVersionCheck", + "nightlyTasks", "notifications", "oauth", "passwordLogin", @@ -14324,6 +15037,37 @@ ], "type": "object" }, + "SystemConfigNightlyTasksDto": { + "properties": { + "clusterNewFaces": { + "type": "boolean" + }, + "databaseCleanup": { + "type": "boolean" + }, + "generateMemories": { + "type": "boolean" + }, + "missingThumbnails": { + "type": "boolean" + }, + "startTime": { + "type": "string" + }, + "syncQuotaUsage": { + "type": "boolean" + } + }, + "required": [ + "clusterNewFaces", + "databaseCleanup", + "generateMemories", + "missingThumbnails", + "startTime", + "syncQuotaUsage" + ], + "type": "object" + }, "SystemConfigNotificationsDto": { "properties": { "smtp": { @@ -14374,6 +15118,9 @@ "profileSigningAlgorithm": { "type": "string" }, + "roleClaim": { + "type": "string" + }, "scope": { "type": "string" }, @@ -14410,6 +15157,7 @@ "mobileOverrideEnabled", "mobileRedirectUri", "profileSigningAlgorithm", + "roleClaim", "scope", "signingAlgorithm", "storageLabelClaim", @@ -15434,6 +16182,14 @@ ], "type": "object" }, + "UserMetadataKey": { + "enum": [ + "preferences", + "license", + "onboarding" + ], + "type": "string" + }, "UserPreferencesResponseDto": { "properties": { "albums": { diff --git a/open-api/templates/mobile/serialization/native/native_class.mustache b/open-api/templates/mobile/serialization/native/native_class.mustache index 9f40d5b0bf..2d6e6d24f3 100644 --- a/open-api/templates/mobile/serialization/native/native_class.mustache +++ b/open-api/templates/mobile/serialization/native/native_class.mustache @@ -206,7 +206,7 @@ class {{{classname}}} { : {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'), {{/isNumber}} {{#isDouble}} - {{{name}}}: (mapValueOfType(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(), + {{{name}}}: (mapValueOfType(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}){{#isNullable}}?{{/isNullable}}.toDouble(), {{/isDouble}} {{^isDouble}} {{^isNumber}} diff --git a/open-api/templates/mobile/serialization/native/native_class.mustache.patch b/open-api/templates/mobile/serialization/native/native_class.mustache.patch index 4ba6594966..8eeefdad97 100644 --- a/open-api/templates/mobile/serialization/native/native_class.mustache.patch +++ b/open-api/templates/mobile/serialization/native/native_class.mustache.patch @@ -1,5 +1,5 @@ ---- native_class.mustache 2024-09-19 11:41:07.855683995 -0400 -+++ native_class_temp.mustache 2024-09-19 11:41:57.113249395 -0400 +--- native_class.mustache 2025-07-01 08:29:23.968133163 +0800 ++++ native_class_temp.mustache 2025-07-01 08:29:44.225850583 +0800 @@ -91,14 +91,14 @@ {{/isDateTime}} {{#isNullable}} @@ -44,7 +44,7 @@ : {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'), {{/isNumber}} + {{#isDouble}} -+ {{{name}}}: (mapValueOfType(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(), ++ {{{name}}}: (mapValueOfType(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}){{#isNullable}}?{{/isNullable}}.toDouble(), + {{/isDouble}} + {{^isDouble}} {{^isNumber}} diff --git a/open-api/typescript-sdk/.nvmrc b/open-api/typescript-sdk/.nvmrc index 5b540673a8..7377d130ed 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -22.16.0 +22.17.1 diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 734cb642dc..61c8807fc5 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,18 +1,18 @@ { "name": "@immich/sdk", - "version": "1.135.3", + "version": "1.136.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.135.3", + "version": "1.136.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.15.31", + "@types/node": "^22.16.4", "typescript": "^5.3.3" } }, @@ -23,9 +23,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.32", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz", - "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==", + "version": "22.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", + "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index f3bf00c9b9..fceb764f82 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.135.3", + "version": "1.136.0", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.15.31", + "@types/node": "^22.16.4", "typescript": "^5.3.3" }, "repository": { @@ -28,6 +28,6 @@ "directory": "open-api/typescript-sdk" }, "volta": { - "node": "22.16.0" + "node": "22.17.1" } } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index fd866d9122..7c030d43c2 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.135.3 + * 1.136.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -889,7 +889,7 @@ export type MetadataSearchDto = { rating?: number; size?: number; state?: string | null; - tagIds?: string[]; + tagIds?: string[] | null; takenAfter?: string; takenBefore?: string; thumbnailPath?: string; @@ -956,7 +956,7 @@ export type RandomSearchDto = { rating?: number; size?: number; state?: string | null; - tagIds?: string[]; + tagIds?: string[] | null; takenAfter?: string; takenBefore?: string; trashedAfter?: string; @@ -993,7 +993,7 @@ export type SmartSearchDto = { rating?: number; size?: number; state?: string | null; - tagIds?: string[]; + tagIds?: string[] | null; takenAfter?: string; takenBefore?: string; trashedAfter?: string; @@ -1025,7 +1025,7 @@ export type StatisticsSearchDto = { personIds?: string[]; rating?: number; state?: string | null; - tagIds?: string[]; + tagIds?: string[] | null; takenAfter?: string; takenBefore?: string; trashedAfter?: string; @@ -1164,6 +1164,7 @@ export type SessionResponseDto = { deviceType: string; expiresAt?: string; id: string; + isPendingSyncReset: boolean; updatedAt: string; }; export type SessionCreateDto = { @@ -1179,9 +1180,13 @@ export type SessionCreateResponseDto = { deviceType: string; expiresAt?: string; id: string; + isPendingSyncReset: boolean; token: string; updatedAt: string; }; +export type SessionUpdateDto = { + isPendingSyncReset?: boolean; +}; export type SharedLinkResponseDto = { album?: AlbumResponseDto; allowDownload: boolean; @@ -1264,6 +1269,7 @@ export type AssetFullSyncDto = { userId?: string; }; export type SyncStreamDto = { + reset?: boolean; types: SyncRequestType[]; }; export type DatabaseBackupConfig = { @@ -1383,6 +1389,14 @@ export type SystemConfigMetadataDto = { export type SystemConfigNewVersionCheckDto = { enabled: boolean; }; +export type SystemConfigNightlyTasksDto = { + clusterNewFaces: boolean; + databaseCleanup: boolean; + generateMemories: boolean; + missingThumbnails: boolean; + startTime: string; + syncQuotaUsage: boolean; +}; export type SystemConfigNotificationsDto = { smtp: SystemConfigSmtpDto; }; @@ -1398,6 +1412,7 @@ export type SystemConfigOAuthDto = { mobileOverrideEnabled: boolean; mobileRedirectUri: string; profileSigningAlgorithm: string; + roleClaim: string; scope: string; signingAlgorithm: string; storageLabelClaim: string; @@ -1450,6 +1465,7 @@ export type SystemConfigDto = { map: SystemConfigMapDto; metadata: SystemConfigMetadataDto; newVersionCheck: SystemConfigNewVersionCheckDto; + nightlyTasks: SystemConfigNightlyTasksDto; notifications: SystemConfigNotificationsDto; oauth: SystemConfigOAuthDto; passwordLogin: SystemConfigPasswordLoginDto; @@ -2769,6 +2785,15 @@ export function updatePartner({ id, updatePartnerDto }: { body: updatePartnerDto }))); } +export function deletePeople({ bulkIdsDto }: { + bulkIdsDto: BulkIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/people", oazapfts.json({ + ...opts, + method: "DELETE", + body: bulkIdsDto + }))); +} export function getAllPeople({ closestAssetId, closestPersonId, page, size, withHidden }: { closestAssetId?: string; closestPersonId?: string; @@ -2813,6 +2838,14 @@ export function updatePeople({ peopleUpdateDto }: { body: peopleUpdateDto }))); } +export function deletePerson({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/people/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} export function getPerson({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { @@ -3152,6 +3185,19 @@ export function deleteSession({ id }: { method: "DELETE" })); } +export function updateSession({ id, sessionUpdateDto }: { + id: string; + sessionUpdateDto: SessionUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SessionResponseDto; + }>(`/sessions/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: sessionUpdateDto + }))); +} export function lockSession({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { @@ -3327,6 +3373,15 @@ export function updateStack({ id, stackUpdateDto }: { body: stackUpdateDto }))); } +export function removeAssetFromStack({ assetId, id }: { + assetId: string; + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/stacks/${encodeURIComponent(id)}/assets/${encodeURIComponent(assetId)}`, { + ...opts, + method: "DELETE" + })); +} export function deleteSyncAck({ syncAckDeleteDto }: { syncAckDeleteDto: SyncAckDeleteDto; }, opts?: Oazapfts.RequestOpts) { @@ -3892,25 +3947,35 @@ export enum Permission { AssetRead = "asset.read", AssetUpdate = "asset.update", AssetDelete = "asset.delete", + AssetStatistics = "asset.statistics", AssetShare = "asset.share", AssetView = "asset.view", AssetDownload = "asset.download", AssetUpload = "asset.upload", + AssetReplace = "asset.replace", AlbumCreate = "album.create", AlbumRead = "album.read", AlbumUpdate = "album.update", AlbumDelete = "album.delete", AlbumStatistics = "album.statistics", - AlbumAddAsset = "album.addAsset", - AlbumRemoveAsset = "album.removeAsset", AlbumShare = "album.share", AlbumDownload = "album.download", + AlbumAssetCreate = "albumAsset.create", + AlbumAssetDelete = "albumAsset.delete", + AlbumUserCreate = "albumUser.create", + AlbumUserUpdate = "albumUser.update", + AlbumUserDelete = "albumUser.delete", + AuthChangePassword = "auth.changePassword", AuthDeviceDelete = "authDevice.delete", ArchiveRead = "archive.read", + DuplicateRead = "duplicate.read", + DuplicateDelete = "duplicate.delete", FaceCreate = "face.create", FaceRead = "face.read", FaceUpdate = "face.update", FaceDelete = "face.delete", + JobCreate = "job.create", + JobRead = "job.read", LibraryCreate = "library.create", LibraryRead = "library.read", LibraryUpdate = "library.update", @@ -3922,6 +3987,9 @@ export enum Permission { MemoryRead = "memory.read", MemoryUpdate = "memory.update", MemoryDelete = "memory.delete", + MemoryStatistics = "memory.statistics", + MemoryAssetCreate = "memoryAsset.create", + MemoryAssetDelete = "memoryAsset.delete", NotificationCreate = "notification.create", NotificationRead = "notification.read", NotificationUpdate = "notification.update", @@ -3937,6 +4005,16 @@ export enum Permission { PersonStatistics = "person.statistics", PersonMerge = "person.merge", PersonReassign = "person.reassign", + PinCodeCreate = "pinCode.create", + PinCodeUpdate = "pinCode.update", + PinCodeDelete = "pinCode.delete", + ServerAbout = "server.about", + ServerApkLinks = "server.apkLinks", + ServerStorage = "server.storage", + ServerStatistics = "server.statistics", + ServerLicenseRead = "serverLicense.read", + ServerLicenseUpdate = "serverLicense.update", + ServerLicenseDelete = "serverLicense.delete", SessionCreate = "session.create", SessionRead = "session.read", SessionUpdate = "session.update", @@ -3950,6 +4028,10 @@ export enum Permission { StackRead = "stack.read", StackUpdate = "stack.update", StackDelete = "stack.delete", + SyncStream = "sync.stream", + SyncCheckpointRead = "syncCheckpoint.read", + SyncCheckpointUpdate = "syncCheckpoint.update", + SyncCheckpointDelete = "syncCheckpoint.delete", SystemConfigRead = "systemConfig.read", SystemConfigUpdate = "systemConfig.update", SystemMetadataRead = "systemMetadata.read", @@ -3959,10 +4041,25 @@ export enum Permission { TagUpdate = "tag.update", TagDelete = "tag.delete", TagAsset = "tag.asset", - AdminUserCreate = "admin.user.create", - AdminUserRead = "admin.user.read", - AdminUserUpdate = "admin.user.update", - AdminUserDelete = "admin.user.delete" + UserRead = "user.read", + UserUpdate = "user.update", + UserLicenseCreate = "userLicense.create", + UserLicenseRead = "userLicense.read", + UserLicenseUpdate = "userLicense.update", + UserLicenseDelete = "userLicense.delete", + UserOnboardingRead = "userOnboarding.read", + UserOnboardingUpdate = "userOnboarding.update", + UserOnboardingDelete = "userOnboarding.delete", + UserPreferenceRead = "userPreference.read", + UserPreferenceUpdate = "userPreference.update", + UserProfileImageCreate = "userProfileImage.create", + UserProfileImageRead = "userProfileImage.read", + UserProfileImageUpdate = "userProfileImage.update", + UserProfileImageDelete = "userProfileImage.delete", + AdminUserCreate = "adminUser.create", + AdminUserRead = "adminUser.read", + AdminUserUpdate = "adminUser.update", + AdminUserDelete = "adminUser.delete" } export enum AssetMediaStatus { Created = "created", @@ -4044,34 +4141,69 @@ export enum Error2 { NotFound = "not_found" } export enum SyncEntityType { + AuthUserV1 = "AuthUserV1", UserV1 = "UserV1", UserDeleteV1 = "UserDeleteV1", - PartnerV1 = "PartnerV1", - PartnerDeleteV1 = "PartnerDeleteV1", AssetV1 = "AssetV1", AssetDeleteV1 = "AssetDeleteV1", AssetExifV1 = "AssetExifV1", + PartnerV1 = "PartnerV1", + PartnerDeleteV1 = "PartnerDeleteV1", PartnerAssetV1 = "PartnerAssetV1", PartnerAssetBackfillV1 = "PartnerAssetBackfillV1", PartnerAssetDeleteV1 = "PartnerAssetDeleteV1", PartnerAssetExifV1 = "PartnerAssetExifV1", PartnerAssetExifBackfillV1 = "PartnerAssetExifBackfillV1", + PartnerStackBackfillV1 = "PartnerStackBackfillV1", + PartnerStackDeleteV1 = "PartnerStackDeleteV1", + PartnerStackV1 = "PartnerStackV1", AlbumV1 = "AlbumV1", AlbumDeleteV1 = "AlbumDeleteV1", AlbumUserV1 = "AlbumUserV1", AlbumUserBackfillV1 = "AlbumUserBackfillV1", AlbumUserDeleteV1 = "AlbumUserDeleteV1", - SyncAckV1 = "SyncAckV1" + AlbumAssetV1 = "AlbumAssetV1", + AlbumAssetBackfillV1 = "AlbumAssetBackfillV1", + AlbumAssetExifV1 = "AlbumAssetExifV1", + AlbumAssetExifBackfillV1 = "AlbumAssetExifBackfillV1", + AlbumToAssetV1 = "AlbumToAssetV1", + AlbumToAssetDeleteV1 = "AlbumToAssetDeleteV1", + AlbumToAssetBackfillV1 = "AlbumToAssetBackfillV1", + MemoryV1 = "MemoryV1", + MemoryDeleteV1 = "MemoryDeleteV1", + MemoryToAssetV1 = "MemoryToAssetV1", + MemoryToAssetDeleteV1 = "MemoryToAssetDeleteV1", + StackV1 = "StackV1", + StackDeleteV1 = "StackDeleteV1", + PersonV1 = "PersonV1", + PersonDeleteV1 = "PersonDeleteV1", + AssetFaceV1 = "AssetFaceV1", + AssetFaceDeleteV1 = "AssetFaceDeleteV1", + UserMetadataV1 = "UserMetadataV1", + UserMetadataDeleteV1 = "UserMetadataDeleteV1", + SyncAckV1 = "SyncAckV1", + SyncResetV1 = "SyncResetV1" } export enum SyncRequestType { - UsersV1 = "UsersV1", - PartnersV1 = "PartnersV1", + AlbumsV1 = "AlbumsV1", + AlbumUsersV1 = "AlbumUsersV1", + AlbumToAssetsV1 = "AlbumToAssetsV1", + AlbumAssetsV1 = "AlbumAssetsV1", + AlbumAssetExifsV1 = "AlbumAssetExifsV1", AssetsV1 = "AssetsV1", AssetExifsV1 = "AssetExifsV1", + AuthUsersV1 = "AuthUsersV1", + MemoriesV1 = "MemoriesV1", + MemoryToAssetsV1 = "MemoryToAssetsV1", + PartnersV1 = "PartnersV1", PartnerAssetsV1 = "PartnerAssetsV1", PartnerAssetExifsV1 = "PartnerAssetExifsV1", - AlbumsV1 = "AlbumsV1", - AlbumUsersV1 = "AlbumUsersV1" + PartnerStacksV1 = "PartnerStacksV1", + StacksV1 = "StacksV1", + UsersV1 = "UsersV1", + PeopleV1 = "PeopleV1", + AssetFacesV1 = "AssetFacesV1", + UserMetadataV1 = "UserMetadataV1" } export enum TranscodeHWAccel { Nvenc = "nvenc", diff --git a/server/.nvmrc b/server/.nvmrc index 5b540673a8..7377d130ed 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -22.16.0 +22.17.1 diff --git a/server/Dockerfile b/server/Dockerfile index 21c2031227..9110d46f7d 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,53 +1,57 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:202505131114@sha256:cf4507bbbf307e9b6d8ee9418993321f2b85867da8ce14d0a20ccaf9574cb995 AS dev +FROM ghcr.io/immich-app/base-server-dev:202507162011@sha256:85d4230c2208646bd6c528db41b2213d780b11b7a311397ca6a2aaba7cf697c8 AS dev -RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app -COPY server/package.json server/package-lock.json ./ -COPY server/patches ./patches +COPY ./server/package* ./server/ +WORKDIR /usr/src/app/server RUN npm ci && \ - # exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need - # they're marked as optional dependencies, so we need to copy them manually after pruning - rm -rf node_modules/@img/sharp-libvips* && \ - rm -rf node_modules/@img/sharp-linuxmusl-x64 -ENV PATH="${PATH}:/usr/src/app/bin" \ - IMMICH_ENV=development \ - NVIDIA_DRIVER_CAPABILITIES=all \ - NVIDIA_VISIBLE_DEVICES=all -ENTRYPOINT ["tini", "--", "/bin/sh"] + # exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need + # they're marked as optional dependencies, so we need to copy them manually after pruning + rm -rf node_modules/@img/sharp-libvips* && \ + rm -rf node_modules/@img/sharp-linuxmusl-x64 +ENV PATH="${PATH}:/usr/src/app/server/bin" \ + IMMICH_ENV=development \ + NVIDIA_DRIVER_CAPABILITIES=all \ + NVIDIA_VISIBLE_DEVICES=all +ENTRYPOINT ["tini", "--", "/bin/bash", "-c"] FROM dev AS dev-container-server +RUN rm -rf /usr/src/app RUN apt-get update && \ - apt-get install sudo inetutils-ping openjdk-11-jre-headless \ - vim nano \ - -y --no-install-recommends --fix-missing + apt-get install sudo inetutils-ping openjdk-11-jre-headless \ + vim nano \ + -y --no-install-recommends --fix-missing RUN usermod -aG sudo node RUN echo "node ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers RUN mkdir -p /workspaces/immich RUN chown node -R /workspaces +COPY --chown=node:node --chmod=777 ../.devcontainer/server/*.sh /immich-devcontainer/ + +USER node +COPY --chown=node:node .. /tmp/create-dep-cache/ +WORKDIR /tmp/create-dep-cache +RUN make ci-all && rm -rf /tmp/create-dep-cache -RUN mkdir /immich-devcontainer && chown node -R /immich-devcontainer -COPY --chmod=777 ../.devcontainer/server/*.sh /immich-devcontainer/ FROM dev-container-server AS dev-container-mobile - +USER root # Enable multiarch for arm64 if necessary RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \ - dpkg --add-architecture amd64 && \ - apt-get update && \ - apt-get install -y --no-install-recommends \ - qemu-user-static \ - libc6:amd64 \ - libstdc++6:amd64 \ - libgcc1:amd64; \ - fi + dpkg --add-architecture amd64 && \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + qemu-user-static \ + libc6:amd64 \ + libstdc++6:amd64 \ + libgcc1:amd64; \ + fi # Flutter SDK # https://flutter.dev/docs/development/tools/sdk/releases?tab=linux ENV FLUTTER_CHANNEL="stable" -ENV FLUTTER_VERSION="3.29.3" +ENV FLUTTER_VERSION="3.32.8" ENV FLUTTER_HOME=/flutter ENV PATH=${PATH}:${FLUTTER_HOME}/bin @@ -74,45 +78,42 @@ FROM dev AS prod COPY server . RUN npm run build RUN npm prune --omit=dev --omit=optional -COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img -COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl +COPY --from=dev /usr/src/app/server/node_modules/@img ./node_modules/@img +COPY --from=dev /usr/src/app/server/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl # web build FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS web -WORKDIR /usr/src/open-api/typescript-sdk -COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ -RUN npm ci -COPY open-api/typescript-sdk/ ./ -RUN npm run build - WORKDIR /usr/src/app -COPY web/package*.json web/svelte.config.js ./ -RUN npm ci -COPY web ./ -COPY i18n ../i18n -RUN npm run build +COPY ./web ./web/ +COPY ./i18n ./i18n/ +COPY ./open-api/typescript-sdk ./open-api/typescript-sdk/ +WORKDIR /usr/src/app/open-api/typescript-sdk +RUN npm ci && npm run build + +WORKDIR /usr/src/app/web +RUN npm ci && npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:202505061115@sha256:9971d3a089787f0bd01f4682141d3665bcf5efb3e101a88e394ffd25bee4eedb +FROM ghcr.io/immich-app/base-server-prod:202507162011@sha256:636f3ddb6106628ef851d51c23f3fa2c6e4829390cc315b27b38c288c82b23a7 WORKDIR /usr/src/app ENV NODE_ENV=production \ - NVIDIA_DRIVER_CAPABILITIES=all \ - NVIDIA_VISIBLE_DEVICES=all -COPY --from=prod /usr/src/app/node_modules ./node_modules -COPY --from=prod /usr/src/app/dist ./dist -COPY --from=prod /usr/src/app/bin ./bin -COPY --from=web /usr/src/app/build /build/www -COPY server/resources resources -COPY server/package.json server/package-lock.json ./ -COPY server/start*.sh ./ -COPY "docker/scripts/get-cpus.sh" ./ -RUN npm install -g @immich/cli && npm cache clean --force + NVIDIA_DRIVER_CAPABILITIES=all \ + NVIDIA_VISIBLE_DEVICES=all + +COPY --from=prod /usr/src/app/server/node_modules ./server/node_modules +COPY --from=prod /usr/src/app/server/dist ./server/dist +COPY --from=prod /usr/src/app/server/bin ./server/bin +COPY --from=web /usr/src/app/web/build /build/www +COPY ./server/resources ./server/resources +COPY ./server/package.json server/package-lock.json ./ COPY LICENSE /licenses/LICENSE.txt COPY LICENSE /LICENSE -ENV PATH="${PATH}:/usr/src/app/bin" + +RUN npm install -g @immich/cli && npm cache clean --force +ENV PATH="${PATH}:/usr/src/app/server/bin" ARG BUILD_ID ARG BUILD_IMAGE @@ -131,7 +132,7 @@ ENV IMMICH_SOURCE_URL=https://github.com/immich-app/immich/commit/${BUILD_SOURCE VOLUME /usr/src/app/upload EXPOSE 2283 -ENTRYPOINT ["tini", "--", "/bin/bash"] +ENTRYPOINT ["tini", "--", "/bin/bash", "-c"] CMD ["start.sh"] HEALTHCHECK CMD immich-healthcheck diff --git a/docker/scripts/get-cpus.sh b/server/bin/get-cpus.sh similarity index 100% rename from docker/scripts/get-cpus.sh rename to server/bin/get-cpus.sh diff --git a/server/bin/immich-admin b/server/bin/immich-admin index 30fd33a20a..0465a362b8 100755 --- a/server/bin/immich-admin +++ b/server/bin/immich-admin @@ -1,3 +1,3 @@ #!/usr/bin/env sh -/usr/src/app/start.sh immich-admin "$@" +start.sh immich-admin "$@" diff --git a/server/bin/immich-dev b/server/bin/immich-dev index 177455d037..e861c0ee06 100755 --- a/server/bin/immich-dev +++ b/server/bin/immich-dev @@ -1,3 +1,9 @@ #!/usr/bin/env bash -node /usr/src/app/node_modules/.bin/nest start --debug "0.0.0.0:9230" --watch -- "$@" +if [ "$IMMICH_ENV" != "development" ]; then + echo "This command can only be run in development environments" + exit 1 +fi + +cd /usr/src/app/server || exit 1 +npm exec nest -- start --debug "0.0.0.0:9230" --watch -- "$@" diff --git a/server/bin/start.sh b/server/bin/start.sh new file mode 100755 index 0000000000..2b4351a6bc --- /dev/null +++ b/server/bin/start.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +echo "Initializing Immich $IMMICH_SOURCE_REF" + +lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2" +if [ -f "$lib_path" ]; then + export LD_PRELOAD="$lib_path" +else + echo "skipping libmimalloc - path not found $lib_path" +fi +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/jellyfin-ffmpeg/lib" +SERVER_HOME=/usr/src/app/server + +read_file_and_export() { + if [ -n "${!1}" ]; then + content="$(cat "${!1}")" + export "$2"="${content}" + unset "$1" + fi +} +read_file_and_export "DB_URL_FILE" "DB_URL" +read_file_and_export "DB_HOSTNAME_FILE" "DB_HOSTNAME" +read_file_and_export "DB_DATABASE_NAME_FILE" "DB_DATABASE_NAME" +read_file_and_export "DB_USERNAME_FILE" "DB_USERNAME" +read_file_and_export "DB_PASSWORD_FILE" "DB_PASSWORD" +read_file_and_export "REDIS_PASSWORD_FILE" "REDIS_PASSWORD" + +if CPU_CORES="${CPU_CORES:=$(get-cpus.sh 2>/dev/null)}"; then + echo "Detected CPU Cores: $CPU_CORES" + if [ "$CPU_CORES" -gt 4 ]; then + export UV_THREADPOOL_SIZE=$CPU_CORES + fi +else + echo "skipping get-cpus.sh - not found in PATH or failed: using default UV_THREADPOOL_SIZE" +fi + +if [ -f "${SERVER_HOME}/dist/main.js" ]; then + exec node "${SERVER_HOME}/dist/main.js" "$@" +else + echo "Error: ${SERVER_HOME}/dist/main.js not found" + if [ "$IMMICH_ENV" = "development" ]; then + echo "You may need to build the server first." + fi + exit 1 +fi diff --git a/server/package-lock.json b/server/package-lock.json index 3a37538223..5b7ffcdcaf 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,13 +1,12 @@ { "name": "immich", - "version": "1.135.3", + "version": "1.136.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.135.3", - "hasInstallScript": true, + "version": "1.136.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^11.0.1", @@ -16,26 +15,38 @@ "@nestjs/event-emitter": "^3.0.0", "@nestjs/platform-express": "^11.0.4", "@nestjs/platform-socket.io": "^11.0.4", - "@nestjs/schedule": "^5.0.0", + "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.0.2", "@nestjs/websockets": "^11.0.4", - "@opentelemetry/auto-instrumentations-node": "^0.59.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/auto-instrumentations-node": "^0.62.0", "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/exporter-prometheus": "^0.201.0", - "@opentelemetry/sdk-node": "^0.201.0", - "@react-email/components": "^0.0.41", + "@opentelemetry/exporter-prometheus": "^0.203.0", + "@opentelemetry/instrumentation-http": "^0.203.0", + "@opentelemetry/instrumentation-ioredis": "^0.51.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.49.0", + "@opentelemetry/instrumentation-pg": "^0.55.0", + "@opentelemetry/resources": "^2.0.1", + "@opentelemetry/sdk-metrics": "^2.0.1", + "@opentelemetry/sdk-node": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@react-email/components": "^0.3.0", + "@react-email/render": "^1.1.2", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", "bcrypt": "^6.0.0", + "body-parser": "^2.2.0", "bullmq": "^5.51.0", - "chokidar": "^3.5.3", + "chokidar": "^4.0.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "compression": "^1.8.0", "cookie": "^1.0.2", "cookie-parser": "^1.4.7", - "exiftool-vendored": "^28.3.1", + "cron": "4.3.0", + "exiftool-vendored": "^28.8.0", + "express": "^5.1.0", "fast-glob": "^3.3.2", "fluent-ffmpeg": "^2.1.2", "geo-tz": "^8.0.0", @@ -43,19 +54,22 @@ "i18n-iso-countries": "^7.6.0", "ioredis": "^5.3.2", "js-yaml": "^4.1.0", - "kysely": "^0.28.0", + "kysely": "^0.28.2", "kysely-postgres-js": "^2.0.0", "lodash": "^4.17.21", "luxon": "^3.4.2", "mnemonist": "^0.40.3", + "multer": "^2.0.1", "nest-commander": "^3.16.0", "nestjs-cls": "^5.0.0", - "nestjs-kysely": "^1.1.0", - "nestjs-otel": "^6.0.0", + "nestjs-kysely": "^3.0.0", + "nestjs-otel": "^7.0.0", "nodemailer": "^7.0.0", "openid-client": "^6.3.3", "pg": "^8.11.3", + "pg-connection-string": "^2.9.1", "picomatch": "^4.0.2", + "postgres": "3.4.7", "react": "^19.0.0", "react-dom": "^19.0.0", "react-email": "^4.0.0", @@ -66,7 +80,8 @@ "semver": "^7.6.2", "sharp": "^0.34.2", "sirv": "^3.0.0", - "tailwindcss-preset-email": "^1.3.2", + "socket.io": "^4.8.1", + "tailwindcss-preset-email": "^1.4.0", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", "ua-parser-js": "^2.0.0", @@ -84,15 +99,17 @@ "@types/archiver": "^6.0.0", "@types/async-lock": "^1.4.2", "@types/bcrypt": "^5.0.0", + "@types/body-parser": "^1.19.6", "@types/compression": "^1.7.5", "@types/cookie-parser": "^1.4.8", "@types/express": "^5.0.0", "@types/fluent-ffmpeg": "^2.1.21", "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.14.197", + "@types/luxon": "^3.6.2", "@types/mock-fs": "^4.13.1", - "@types/multer": "^1.4.7", - "@types/node": "^22.15.31", + "@types/multer": "^2.0.0", + "@types/node": "^22.16.4", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", @@ -101,17 +118,17 @@ "@types/semver": "^7.5.8", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", + "@types/validator": "^13.15.2", "@vitest/coverage-v8": "^3.0.0", + "canvas": "^3.1.0", "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^59.0.0", "globals": "^16.0.0", - "jsdom": "^26.1.0", "mock-fs": "^5.2.0", "node-addon-api": "^8.3.1", "node-gyp": "^11.2.0", - "patch-package": "^8.0.0", "pngjs": "^7.0.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^4.0.0", @@ -119,6 +136,7 @@ "source-map-support": "^0.5.21", "sql-formatter": "^15.0.0", "supertest": "^7.1.0", + "tailwindcss": "^3.4.0", "testcontainers": "^11.0.0", "tsconfig-paths": "^4.2.0", "typescript": "^5.3.3", @@ -134,7 +152,6 @@ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -157,13 +174,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.6.tgz", - "integrity": "sha512-YTAxNnT++5eflx19OUHmOWu597/TbTel+QARiZCv1xQw99+X8DCKKOUXtqBRd53CAHlREDI33Rn/JLY3NYgMLQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.8.tgz", + "integrity": "sha512-QsmFuYdAyeCyg9WF/AJBhFXDUfCwmDFTEbsv5t5KPSP6slhk0GoLNZApniiFytU2siRlSxVNpve2uATyYuAYkQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.6", + "@angular-devkit/core": "19.2.8", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -226,25 +243,6 @@ } } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/schematics": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.8.tgz", - "integrity": "sha512-QsmFuYdAyeCyg9WF/AJBhFXDUfCwmDFTEbsv5t5KPSP6slhk0GoLNZApniiFytU2siRlSxVNpve2uATyYuAYkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "19.2.8", - "jsonc-parser": "3.3.1", - "magic-string": "0.30.17", - "ora": "5.4.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", @@ -292,24 +290,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@angular-devkit/schematics-cli/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -317,20 +297,17 @@ "dev": true, "license": "MIT" }, - "node_modules/@angular-devkit/schematics-cli/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "node_modules/@angular-devkit/schematics-cli/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "engines": { - "node": ">= 14.18.0" + "node": ">=12" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@angular-devkit/schematics-cli/node_modules/rxjs": { @@ -354,9 +331,9 @@ } }, "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.6.tgz", - "integrity": "sha512-WFgiYhrDMq83UNaGRAneIM7CYYdBozD+yYA9BjoU8AgBLKtrvn6S8ZcjKAk5heoHtY/u8pEb0mwDTz9gxFmJZQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.8.tgz", + "integrity": "sha512-kcxUHKf5Hi98r4gAvMP3ntJV8wuQ3/i6wuU9RcMP0UKUt2Rer5Ryis3MPqT92jvVVwg6lhrLIhXsFuWJMiYjXQ==", "dev": true, "license": "MIT", "dependencies": { @@ -398,24 +375,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@angular-devkit/schematics/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@angular-devkit/schematics/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -423,20 +382,17 @@ "dev": true, "license": "MIT" }, - "node_modules/@angular-devkit/schematics/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "node_modules/@angular-devkit/schematics/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "engines": { - "node": ">= 14.18.0" + "node": ">=12" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@angular-devkit/schematics/node_modules/rxjs": { @@ -460,11 +416,13 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.4.tgz", - "integrity": "sha512-SeuBV4rnjpFNjI8HSgKUwteuFdkHwkboq31HWzznuqgySQir+jSTczoWVVL4jvOjKjuH80fMDG0Fvg1Sb+OJsA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", @@ -473,35 +431,34 @@ "lru-cache": "^10.4.3" } }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, "node_modules/@babel/generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", - "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0", + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -529,9 +486,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.3.tgz", - "integrity": "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -544,30 +501,30 @@ } }, "node_modules/@babel/template": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", - "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -585,9 +542,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", - "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -641,14 +598,16 @@ } ], "license": "MIT-0", + "optional": true, + "peer": true, "engines": { "node": ">=18" } }, "node_modules/@csstools/css-calc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.3.tgz", - "integrity": "sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", "dev": true, "funding": [ { @@ -661,18 +620,20 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.9.tgz", - "integrity": "sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", "dev": true, "funding": [ { @@ -685,22 +646,24 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@csstools/color-helpers": "^5.0.2", - "@csstools/css-calc": "^2.1.3" + "@csstools/css-calc": "^2.1.4" }, "engines": { "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", - "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, "funding": [ { @@ -713,17 +676,19 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", - "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true, "funding": [ { @@ -736,6 +701,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -1193,9 +1160,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1208,9 +1175,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1218,9 +1185,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1268,9 +1235,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { @@ -1291,13 +1258,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", + "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.0", "levn": "^0.4.1" }, "engines": { @@ -1318,9 +1285,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz", - "integrity": "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", + "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", "license": "Apache-2.0", "dependencies": { "@grpc/proto-loader": "^0.7.13", @@ -1331,9 +1298,9 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", - "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", "license": "Apache-2.0", "dependencies": { "lodash.camelcase": "^4.3.0", @@ -1401,9 +1368,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1811,15 +1778,15 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.5.tgz", - "integrity": "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.8.tgz", + "integrity": "sha512-d/QAsnwuHX2OPolxvYcgSj7A9DO9H6gVOy2DvBTx+P2LH2iRTo/RSGV3iwCzW024nP9hw98KIuDmdyhZQj1UQg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -1836,14 +1803,14 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz", - "integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==", + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.12.tgz", + "integrity": "sha512-dpq+ielV9/bqgXRUbNH//KsY6WEw9DrGPmipkpmgC1Y46cwuBTNx7PXFWTjc3MQ+urcc0QxoVHcMI0FW4Ok0hg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6" + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" }, "engines": { "node": ">=18" @@ -1858,14 +1825,14 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.10", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz", - "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==", + "version": "10.1.13", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", + "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", @@ -1886,14 +1853,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.10.tgz", - "integrity": "sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.13.tgz", + "integrity": "sha512-WbicD9SUQt/K8O5Vyk9iC2ojq5RHoCLK6itpp2fHsWe44VxxcA9z3GTWlvjSTGmMQpZr+lbVmrxdHcumJoLbMA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6", + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", "external-editor": "^3.1.0" }, "engines": { @@ -1909,14 +1876,14 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.12.tgz", - "integrity": "sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.15.tgz", + "integrity": "sha512-4Y+pbr/U9Qcvf+N/goHzPEXiHH8680lM3Dr3Y9h9FFw4gHS+zVpbj8LfbKWIb/jayIB4aSO4pWiBTrBYWkvi5A==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6", + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -1932,9 +1899,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", - "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", + "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", "dev": true, "license": "MIT", "engines": { @@ -1942,14 +1909,14 @@ } }, "node_modules/@inquirer/input": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.9.tgz", - "integrity": "sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.12.tgz", + "integrity": "sha512-xJ6PFZpDjC+tC1P8ImGprgcsrzQRsUh9aH3IZixm1lAZFK49UGHxM3ltFfuInN2kPYNfyoPRh+tU4ftsjPLKqQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6" + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" }, "engines": { "node": ">=18" @@ -1964,14 +1931,14 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.12.tgz", - "integrity": "sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==", + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.15.tgz", + "integrity": "sha512-xWg+iYfqdhRiM55MvqiTCleHzszpoigUpN5+t1OMcRkJrUrw7va3AzXaxvS+Ak7Gny0j2mFSTv2JJj8sMtbV2g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6" + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" }, "engines": { "node": ">=18" @@ -1986,14 +1953,14 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.12.tgz", - "integrity": "sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.15.tgz", + "integrity": "sha512-75CT2p43DGEnfGTaqFpbDC2p2EEMrq0S+IRrf9iJvYreMy5mAWj087+mdKyLHapUEPLjN10mNvABpGbk8Wdraw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6", + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2" }, "engines": { @@ -2039,14 +2006,14 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.12.tgz", - "integrity": "sha512-wNPJZy8Oc7RyGISPxp9/MpTOqX8lr0r+lCCWm7hQra+MDtYRgINv1hxw7R+vKP71Bu/3LszabxOodfV/uTfsaA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.3.tgz", + "integrity": "sha512-7XrV//6kwYumNDSsvJIPeAqa8+p7GJh7H5kRuxirct2cgOcSWwwNGoXDRgpNFbY/MG2vQ4ccIWCi8+IXXyFMZA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6", + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -2062,15 +2029,15 @@ } }, "node_modules/@inquirer/search": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.12.tgz", - "integrity": "sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==", + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.15.tgz", + "integrity": "sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -2086,15 +2053,15 @@ } }, "node_modules/@inquirer/select": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.1.1.tgz", - "integrity": "sha512-IUXzzTKVdiVNMA+2yUvPxWsSgOG4kfX93jOM4Zb5FgujeInotv5SPIJVeXQ+fO4xu7tW8VowFhdG5JRmmCyQ1Q==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.3.tgz", + "integrity": "sha512-OAGhXU0Cvh0PhLz9xTF/kx6g6x+sP+PcyTiLvCrewI99P3BBeexD+VbuwkNDvqGkk3y2h5ZiWLeRP7BFlhkUDg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -2111,9 +2078,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz", - "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", "dev": true, "license": "MIT", "engines": { @@ -2134,6 +2101,27 @@ "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", "license": "MIT" }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2325,6 +2313,62 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@mapbox/node-pre-gyp/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2347,6 +2391,112 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -2364,12 +2514,89 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/@microsoft/tsdoc": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", "license": "MIT" }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", @@ -2383,6 +2610,19 @@ "linux" ] }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@nestjs/bull-shared": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-11.0.2.tgz", @@ -2485,25 +2725,6 @@ } } }, - "node_modules/@nestjs/cli/node_modules/@angular-devkit/schematics": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.8.tgz", - "integrity": "sha512-QsmFuYdAyeCyg9WF/AJBhFXDUfCwmDFTEbsv5t5KPSP6slhk0GoLNZApniiFytU2siRlSxVNpve2uATyYuAYkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "19.2.8", - "jsonc-parser": "3.3.1", - "magic-string": "0.30.17", - "ora": "5.4.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, "node_modules/@nestjs/cli/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -2521,22 +2742,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@nestjs/cli/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nestjs/cli/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -2544,18 +2749,17 @@ "dev": true, "license": "MIT" }, - "node_modules/@nestjs/cli/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "node_modules/@nestjs/cli/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.18.0" + "node": ">=12" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@nestjs/cli/node_modules/rxjs": { @@ -2579,12 +2783,12 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.1.tgz", - "integrity": "sha512-crzp+1qeZ5EGL0nFTPy9NrVMAaUWewV5AwtQyv6SQ9yQPXwRl9W9hm1pt0nAtUu5QbYMbSuo7lYcF81EjM+nCA==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.5.tgz", + "integrity": "sha512-DQpWdr3ShO0BHWkHl3I4W/jR6R3pDtxyBlmrpTuZF+PXxQyBXNvsUne0Wyo6QHPEDi+pAz9XchBFoKbqOhcdTg==", "license": "MIT", "dependencies": { - "file-type": "20.5.0", + "file-type": "21.0.0", "iterare": "1.2.1", "load-esm": "1.0.2", "tslib": "2.8.1", @@ -2610,9 +2814,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.1.tgz", - "integrity": "sha512-UFoUAgLKFT+RwHTANJdr0dF7p0qS9QjkaUPjg8aafnjM/qxxxrUVDB49nVvyMlk+Hr1+vvcNaOHbWWQBxoZcHA==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.5.tgz", + "integrity": "sha512-Qr25MEY9t8VsMETy7eXQ0cNXqu0lzuFrrTr+f+1G57ABCtV5Pogm7n9bF71OU2bnkDD32Bi4hQLeFR90cku3Tw==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -2684,14 +2888,14 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.1.tgz", - "integrity": "sha512-IUxk380qnUtz0PCRQ5i+o9UHlGMrFzGPIJxDwyt3JZZwx2AngOlcEcm5e+7YeJQEr2QYX2QyC4tUQg0zde+D7A==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.5.tgz", + "integrity": "sha512-OsoiUBY9Shs5IG3uvDIt9/IDfY5OlvWBESuB/K4Eun8xILw1EK5d5qMfC3d2sIJ+kA3l+kBR1d/RuzH7VprLIg==", "license": "MIT", "dependencies": { "cors": "2.8.5", "express": "5.1.0", - "multer": "1.4.5-lts.2", + "multer": "2.0.2", "path-to-regexp": "8.2.0", "tslib": "2.8.1" }, @@ -2705,9 +2909,9 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.1.tgz", - "integrity": "sha512-Bsc8ouysUFasWiO8RKEvppqYM5LNkHfbyIJQTy3V6+PUdYhblkvmOq8QtjuHpv6DiBI4siUcxACx/90/CdXLkQ==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.5.tgz", + "integrity": "sha512-DY3zNY+BjbrYpV/t8HL8ptrusrWK8J0cfkfY1iZsfCd+0/1+j8IKno+QMLkerNQAZ7/Frh5tkaKHVwWk18TkMw==", "license": "MIT", "dependencies": { "socket.io": "4.8.1", @@ -2724,12 +2928,12 @@ } }, "node_modules/@nestjs/schedule": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-5.0.1.tgz", - "integrity": "sha512-kFoel84I4RyS2LNPH6yHYTKxB16tb3auAEciFuc788C3ph6nABkUfzX5IE+unjVaRX+3GuruJwurNepMlHXpQg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.0.tgz", + "integrity": "sha512-aQySMw6tw2nhitELXd3EiRacQRgzUKD9mFcUZVOJ7jPLqIBvXOyvRWLsK9SdurGA+jjziAlMef7iB5ZEFFoQpw==", "license": "MIT", "dependencies": { - "cron": "3.5.0" + "cron": "4.3.0" }, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", @@ -2781,6 +2985,25 @@ } } }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.6.tgz", + "integrity": "sha512-YTAxNnT++5eflx19OUHmOWu597/TbTel+QARiZCv1xQw99+X8DCKKOUXtqBRd53CAHlREDI33Rn/JLY3NYgMLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.6", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, "node_modules/@nestjs/schematics/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -2798,24 +3021,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@nestjs/schematics/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nestjs/schematics/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -2823,20 +3028,17 @@ "dev": true, "license": "MIT" }, - "node_modules/@nestjs/schematics/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "node_modules/@nestjs/schematics/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "engines": { - "node": ">= 14.18.0" + "node": ">=12" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@nestjs/schematics/node_modules/rxjs": { @@ -2893,9 +3095,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.1.tgz", - "integrity": "sha512-stzm8YrLDGAijHYQw+8Z9dD6lGdvahL0hIjGVZ/0KBxLZht0/rvRjgV31UK+DUqXaF7yhJTw9ryrPaITxI1J6A==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.5.tgz", + "integrity": "sha512-ZYRYF750SefmuIo7ZqPlHDcin1OHh6My0OkOfGEFjrD9mJ0vMVIpwMTOOkpzCfCcpqUuxeHBuecpiIn+NLrQbQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2921,9 +3123,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.1.tgz", - "integrity": "sha512-gxwQoGx5bW5IvparzrX1UOGXz87eqY0fK5Y6yb14z6tSSubQTciNjCDm5osDEkRyRCG6ZB0F+eXF6dRUjwTlBQ==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.5.tgz", + "integrity": "sha512-mAM11HwyS7aeSUbXdOqHbNCRoHwB0OOb+cmx5sgxvszhdG0Y6bwR60nKA4+EXL9xUEeWoxmbfLmSHlTSIJ9GKA==", "license": "MIT", "dependencies": { "iterare": "1.2.1", @@ -2943,140 +3145,6 @@ } } }, - "node_modules/@next/env": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.3.tgz", - "integrity": "sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw==", - "license": "MIT" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.3.tgz", - "integrity": "sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz", - "integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz", - "integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz", - "integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz", - "integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz", - "integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz", - "integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz", - "integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -3142,37 +3210,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/@npmcli/agent/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/@npmcli/fs": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", @@ -3211,10 +3248,73 @@ "node": ">=8.0.0" } }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.201.1.tgz", - "integrity": "sha512-IxcFDP1IGMDemVFG2by/AMK+/o6EuBQ8idUq3xZ6MxgQGeumYZuX5OwR0h9HuvcUc/JPjQGfU5OHKIKYDJcXeA==", + "node_modules/@opentelemetry/auto-instrumentations-node": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.62.0.tgz", + "integrity": "sha512-h5g+VNJjiyX6u/IQpn36ZCHOENg1QW0GgBOHBcFGnHBBhmTww4R3brExdeuYbvLj3UQY09n+UHFEoMOqkhq07A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/instrumentation-amqplib": "^0.50.0", + "@opentelemetry/instrumentation-aws-lambda": "^0.54.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.56.0", + "@opentelemetry/instrumentation-bunyan": "^0.49.0", + "@opentelemetry/instrumentation-cassandra-driver": "^0.49.0", + "@opentelemetry/instrumentation-connect": "^0.47.0", + "@opentelemetry/instrumentation-cucumber": "^0.18.0", + "@opentelemetry/instrumentation-dataloader": "^0.21.0", + "@opentelemetry/instrumentation-dns": "^0.47.0", + "@opentelemetry/instrumentation-express": "^0.52.0", + "@opentelemetry/instrumentation-fastify": "^0.48.0", + "@opentelemetry/instrumentation-fs": "^0.23.0", + "@opentelemetry/instrumentation-generic-pool": "^0.47.0", + "@opentelemetry/instrumentation-graphql": "^0.51.0", + "@opentelemetry/instrumentation-grpc": "^0.203.0", + "@opentelemetry/instrumentation-hapi": "^0.50.0", + "@opentelemetry/instrumentation-http": "^0.203.0", + "@opentelemetry/instrumentation-ioredis": "^0.51.0", + "@opentelemetry/instrumentation-kafkajs": "^0.12.0", + "@opentelemetry/instrumentation-knex": "^0.48.0", + "@opentelemetry/instrumentation-koa": "^0.51.0", + "@opentelemetry/instrumentation-lru-memoizer": "^0.48.0", + "@opentelemetry/instrumentation-memcached": "^0.47.0", + "@opentelemetry/instrumentation-mongodb": "^0.56.0", + "@opentelemetry/instrumentation-mongoose": "^0.50.0", + "@opentelemetry/instrumentation-mysql": "^0.49.0", + "@opentelemetry/instrumentation-mysql2": "^0.49.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.49.0", + "@opentelemetry/instrumentation-net": "^0.47.0", + "@opentelemetry/instrumentation-oracledb": "^0.29.0", + "@opentelemetry/instrumentation-pg": "^0.55.0", + "@opentelemetry/instrumentation-pino": "^0.50.0", + "@opentelemetry/instrumentation-redis": "^0.51.0", + "@opentelemetry/instrumentation-restify": "^0.49.0", + "@opentelemetry/instrumentation-router": "^0.48.0", + "@opentelemetry/instrumentation-runtime-node": "^0.17.0", + "@opentelemetry/instrumentation-socket.io": "^0.50.0", + "@opentelemetry/instrumentation-tedious": "^0.22.0", + "@opentelemetry/instrumentation-undici": "^0.14.0", + "@opentelemetry/instrumentation-winston": "^0.48.0", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.31.3", + "@opentelemetry/resource-detector-aws": "^2.3.0", + "@opentelemetry/resource-detector-azure": "^0.10.0", + "@opentelemetry/resource-detector-container": "^0.7.3", + "@opentelemetry/resource-detector-gcp": "^0.37.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-node": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^2.0.0" + } + }, + "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -3223,68 +3323,21 @@ "node": ">=8.0.0" } }, - "node_modules/@opentelemetry/auto-instrumentations-node": { - "version": "0.59.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.59.0.tgz", - "integrity": "sha512-kqoEBQss8fGGGRND0ycXZrwCXa/ePFop6W+YvZF5PikA9EsH0J/F2W6zvjetKjtdjyl6AUDW8I7gslZPXLLz3Q==", + "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", - "@opentelemetry/instrumentation-amqplib": "^0.48.0", - "@opentelemetry/instrumentation-aws-lambda": "^0.52.0", - "@opentelemetry/instrumentation-aws-sdk": "^0.53.0", - "@opentelemetry/instrumentation-bunyan": "^0.47.0", - "@opentelemetry/instrumentation-cassandra-driver": "^0.47.0", - "@opentelemetry/instrumentation-connect": "^0.45.0", - "@opentelemetry/instrumentation-cucumber": "^0.16.0", - "@opentelemetry/instrumentation-dataloader": "^0.18.0", - "@opentelemetry/instrumentation-dns": "^0.45.0", - "@opentelemetry/instrumentation-express": "^0.50.0", - "@opentelemetry/instrumentation-fastify": "^0.46.0", - "@opentelemetry/instrumentation-fs": "^0.21.0", - "@opentelemetry/instrumentation-generic-pool": "^0.45.0", - "@opentelemetry/instrumentation-graphql": "^0.49.0", - "@opentelemetry/instrumentation-grpc": "^0.201.0", - "@opentelemetry/instrumentation-hapi": "^0.47.0", - "@opentelemetry/instrumentation-http": "^0.201.0", - "@opentelemetry/instrumentation-ioredis": "^0.49.0", - "@opentelemetry/instrumentation-kafkajs": "^0.10.0", - "@opentelemetry/instrumentation-knex": "^0.46.0", - "@opentelemetry/instrumentation-koa": "^0.49.0", - "@opentelemetry/instrumentation-lru-memoizer": "^0.46.0", - "@opentelemetry/instrumentation-memcached": "^0.45.0", - "@opentelemetry/instrumentation-mongodb": "^0.54.0", - "@opentelemetry/instrumentation-mongoose": "^0.48.0", - "@opentelemetry/instrumentation-mysql": "^0.47.0", - "@opentelemetry/instrumentation-mysql2": "^0.47.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.47.0", - "@opentelemetry/instrumentation-net": "^0.45.0", - "@opentelemetry/instrumentation-oracledb": "^0.27.0", - "@opentelemetry/instrumentation-pg": "^0.53.0", - "@opentelemetry/instrumentation-pino": "^0.48.0", - "@opentelemetry/instrumentation-redis": "^0.48.0", - "@opentelemetry/instrumentation-redis-4": "^0.48.0", - "@opentelemetry/instrumentation-restify": "^0.47.0", - "@opentelemetry/instrumentation-router": "^0.46.0", - "@opentelemetry/instrumentation-runtime-node": "^0.15.0", - "@opentelemetry/instrumentation-socket.io": "^0.48.0", - "@opentelemetry/instrumentation-tedious": "^0.20.0", - "@opentelemetry/instrumentation-undici": "^0.12.0", - "@opentelemetry/instrumentation-winston": "^0.46.0", - "@opentelemetry/resource-detector-alibaba-cloud": "^0.31.1", - "@opentelemetry/resource-detector-aws": "^2.1.0", - "@opentelemetry/resource-detector-azure": "^0.8.0", - "@opentelemetry/resource-detector-container": "^0.7.1", - "@opentelemetry/resource-detector-gcp": "^0.35.0", - "@opentelemetry/resources": "^2.0.0", - "@opentelemetry/sdk-node": "^0.201.0" + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.4.1", - "@opentelemetry/core": "^2.0.0" + "@opentelemetry/api": "^1.3.0" } }, "node_modules/@opentelemetry/context-async-hooks": { @@ -3315,17 +3368,17 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.201.1.tgz", - "integrity": "sha512-ACV2Az9BHRcAaPMYBnYMwKHNn2JwkzzsT3cdeG6+Tokm47fFfpf2xk3sq3QvX0Gk+TXW7q6d+OfBuYfWoAud2g==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.203.0.tgz", + "integrity": "sha512-g/2Y2noc/l96zmM+g0LdeuyYKINyBwN6FJySoU15LHPLcMN/1a0wNk2SegwKcxrRdE7Xsm7fkIR5n6XFe3QpPw==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.1", - "@opentelemetry/otlp-exporter-base": "0.201.1", - "@opentelemetry/otlp-grpc-exporter-base": "0.201.1", - "@opentelemetry/otlp-transformer": "0.201.1", - "@opentelemetry/sdk-logs": "0.201.1" + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", + "@opentelemetry/sdk-logs": "0.203.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3335,16 +3388,16 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.201.1.tgz", - "integrity": "sha512-flYr1tr/wlUxsVc2ZYt/seNLgp3uagyUg9MtjiHYyaMQcN4XuEuI4UjUFwXAGQjd2khmXeie5YnTmO8gzyzemw==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.203.0.tgz", + "integrity": "sha512-s0hys1ljqlMTbXx2XiplmMJg9wG570Z5lH7wMvrZX6lcODI56sG4HL03jklF63tBeyNwK2RV1/ntXGo3HgG4Qw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.201.1", + "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", - "@opentelemetry/otlp-exporter-base": "0.201.1", - "@opentelemetry/otlp-transformer": "0.201.1", - "@opentelemetry/sdk-logs": "0.201.1" + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", + "@opentelemetry/sdk-logs": "0.203.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3353,18 +3406,30 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.201.1.tgz", - "integrity": "sha512-ZVkutDoQYLAkWmpbmd9XKZ9NeBQS6GPxLl/NZ/uDMq+tFnmZu1p0cvZ43x5+TpFoGkjPR6QYHCxkcZBwI9M8ag==", + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.201.1", + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.203.0.tgz", + "integrity": "sha512-nl/7S91MXn5R1aIzoWtMKGvqxgJgepB/sH9qW0rZvZtabnsjbf8OQ1uSx3yogtvLr0GzwD596nQKz2fV7q2RBw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", - "@opentelemetry/otlp-exporter-base": "0.201.1", - "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", - "@opentelemetry/sdk-logs": "0.201.1", + "@opentelemetry/sdk-logs": "0.203.0", "@opentelemetry/sdk-trace-base": "2.0.1" }, "engines": { @@ -3374,18 +3439,30 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.201.1.tgz", - "integrity": "sha512-ywo4TpQNOLi07K7P3CaymzS8XlDGfTFmMQ4oSPsZv38/gAf3/wPVh2uL5qYAFqrVokNCmkcaeCwX3QSy0g9b/A==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.203.0.tgz", + "integrity": "sha512-FCCj9nVZpumPQSEI57jRAA89hQQgONuoC35Lt+rayWY/mzCAc6BQT7RFyFaZKJ2B7IQ8kYjOCPsF/HGFWjdQkQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.201.1", - "@opentelemetry/otlp-exporter-base": "0.201.1", - "@opentelemetry/otlp-grpc-exporter-base": "0.201.1", - "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.203.0", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-metrics": "2.0.1" }, @@ -3397,14 +3474,14 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.201.1.tgz", - "integrity": "sha512-LMRVg2yTev28L51RLLUK3gY0avMa1RVBq7IkYNtXDBxJRcd0TGGq/0rqfk7Y4UIM9NCJhDIUFHeGg8NpSgSWcw==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.203.0.tgz", + "integrity": "sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", - "@opentelemetry/otlp-exporter-base": "0.201.1", - "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-metrics": "2.0.1" }, @@ -3416,15 +3493,15 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.201.1.tgz", - "integrity": "sha512-9ie2jcaUQZdIoe6B02r0rF4Gz+JsZ9mev/2pYou1N0woOUkFM8xwO6BAlORnrFVslqF/XO5WG3q5FsTbuC5iiw==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.203.0.tgz", + "integrity": "sha512-OZnhyd9npU7QbyuHXFEPVm3LnjZYifuKpT3kTnF84mXeEQ84pJJZgyLBpU4FSkSwUkt/zbMyNAI7y5+jYTWGIg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.201.1", - "@opentelemetry/otlp-exporter-base": "0.201.1", - "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.203.0", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-metrics": "2.0.1" }, @@ -3436,9 +3513,9 @@ } }, "node_modules/@opentelemetry/exporter-prometheus": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.201.1.tgz", - "integrity": "sha512-J6/4KgljApWda/2YBMHHZg6vaZ6H8BjFInO8YQW+N0al1LjGAAq3pFRCEHpU6GI7ZlkphCxKy6MUjXOZVM8KWQ==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.203.0.tgz", + "integrity": "sha512-2jLuNuw5m4sUj/SncDf/mFPabUxMZmmYetx5RKIMIQyPnl6G6ooFzfeE8aXNRf8YD1ZXNlCnRPcISxjveGJHNg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", @@ -3453,16 +3530,16 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.201.1.tgz", - "integrity": "sha512-0ZM5CBoZbufXckxi/SWwP5B++CjPWS6N1i+K7f+GhRxYWVGt/yh4eiV3jklZKWw/DUyMkUvUOo0GW1RxoiLoZQ==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.203.0.tgz", + "integrity": "sha512-322coOTf81bm6cAA8+ML6A+m4r2xTCdmAZzGNTboPXRzhwPt4JEmovsFAs+grpdarObd68msOJ9FfH3jxM6wqA==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.1", - "@opentelemetry/otlp-exporter-base": "0.201.1", - "@opentelemetry/otlp-grpc-exporter-base": "0.201.1", - "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, @@ -3474,14 +3551,14 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.201.1.tgz", - "integrity": "sha512-Nw3pIqATC/9LfSGrMiQeeMQ7/z7W2D0wKPxtXwAcr7P64JW7KSH4YSX7Ji8Ti3MmB79NQg6imdagfegJDB0rng==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.203.0.tgz", + "integrity": "sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", - "@opentelemetry/otlp-exporter-base": "0.201.1", - "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, @@ -3493,14 +3570,14 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.201.1.tgz", - "integrity": "sha512-wMxdDDyW+lmmenYGBp0evCoKzajXqIw6SSaZtaF/uqKR9/POhC/9vudnc+kf8W49hYFyIEutPrc1hA0exe3UwQ==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.203.0.tgz", + "integrity": "sha512-1xwNTJ86L0aJmWRwENCJlH4LULMG2sOXWIVw+Szta4fkqKVY50Eo4HoVKKq6U9QEytrWCr8+zjw0q/ZOeXpcAQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", - "@opentelemetry/otlp-exporter-base": "0.201.1", - "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, @@ -3530,32 +3607,13 @@ } }, "node_modules/@opentelemetry/host-metrics": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@opentelemetry/host-metrics/-/host-metrics-0.35.5.tgz", - "integrity": "sha512-Zf9Cjl7H6JalspnK5KD1+LLKSVecSinouVctNmUxRy+WP+20KwHq+qg4hADllkEmJ99MZByLLmEmzrr7s92V6g==", + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/host-metrics/-/host-metrics-0.36.0.tgz", + "integrity": "sha512-14lNY57qa21V3ZOl6xrqLMHR0HGlnPIApR6hr3oCw/Dqs5IzxhTwt2X8Stn82vWJJis7j/ezn11oODsizHj2dQ==", "license": "Apache-2.0", "dependencies": { "systeminformation": "5.23.8" }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.201.1.tgz", - "integrity": "sha512-6EOSoT2zcyBM3VryAzn35ytjRrOMeaWZyzQ/PHVfxoXp5rMf7UUgVToqxOhQffKOHtC7Dma4bHt+DuwIBBZyZA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.201.1", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "shimmer": "^1.2.1" - }, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -3564,13 +3622,13 @@ } }, "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.48.0.tgz", - "integrity": "sha512-zXcClQX3sttvBih1CjdPbvve/If1lCHPFK41fDpJE5NYjK38dwTMOUEV0+/ulfq4iU4oEV+ReCA+ZaXAm/uYdw==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.50.0.tgz", + "integrity": "sha512-kwNs/itehHG/qaQBcVrLNcvXVPW0I4FCOVtw3LHMLdYIqD7GJ6Yv2nX+a4YHjzbzIeRYj8iyMp0Bl7tlkidq5w==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3580,15 +3638,73 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-amqplib/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-amqplib/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-aws-lambda": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.52.0.tgz", - "integrity": "sha512-xGVhBxxO7OuOl72XNwt1MOgaA6d3pSKI2Y5r3OfGNkx602KzW1t2vBHzJf8s4DAJYdMd5/RJLRi1z87CBu7yyg==", + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.54.0.tgz", + "integrity": "sha512-uiYI+kcMUJ/H9cxAwB8c9CaG8behLRgcYSOEA8M/tMQ54Y1ZmzAuEE3QKOi21/s30x5Q+by9g7BwiVfDtqzeMA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/aws-lambda": "8.10.147" + "@types/aws-lambda": "8.10.150" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-aws-lambda/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-aws-lambda/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3598,15 +3714,44 @@ } }, "node_modules/@opentelemetry/instrumentation-aws-sdk": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.53.0.tgz", - "integrity": "sha512-CXB2cu0qnp5lHtNZRpvz0oOZrIKiWfHOiNVGWln9KY0m9sBheEqc58x3Ptpi5lMyso67heVCGDAc9+KbLAZwTQ==", + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.56.0.tgz", + "integrity": "sha512-Jl2B/FYEb6tBCk9G31CMomKPikGU2g+CEhrGddDI0o1YeNpg3kAO9dExF+w489/IJUGZX6/wudyNvV7z4k9NjQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0", - "@opentelemetry/propagation-utils": "^0.31.1", - "@opentelemetry/semantic-conventions": "^1.31.0" + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/propagation-utils": "^0.31.3", + "@opentelemetry/semantic-conventions": "^1.34.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-aws-sdk/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-aws-sdk/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3616,13 +3761,13 @@ } }, "node_modules/@opentelemetry/instrumentation-bunyan": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.47.0.tgz", - "integrity": "sha512-Sux5us8fkBLO/z+H8P2fSu+fRIm1xTeUHlwtM/E4CNZS9W/sAYrc8djZVa2JrwNXj/tE6U5vRJVObGekIkULow==", + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.49.0.tgz", + "integrity": "sha512-ky5Am1y6s3Ex/3RygHxB/ZXNG07zPfg9Z6Ora+vfeKcr/+I6CJbWXWhSBJor3gFgKN3RvC11UWVURnmDpBS6Pg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.201.0", - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/api-logs": "^0.203.0", + "@opentelemetry/instrumentation": "^0.203.0", "@types/bunyan": "1.8.11" }, "engines": { @@ -3632,13 +3777,42 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-cassandra-driver": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.47.0.tgz", - "integrity": "sha512-MMn/Y2ErClGe7fmzTfR3iJcbEIspAn9hxbnj8oH7bVpPHcWbPphYICkNfLqah4tKVd+zazhs1agCiHL8y/e12g==", + "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-cassandra-driver": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.49.0.tgz", + "integrity": "sha512-BNIvqldmLkeikfI5w5Rlm9vG5NnQexfPoxOgEMzfDVOEF+vS6351I6DzWLLgWWR9CNF/jQJJi/lr6am2DLp0Rw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3648,14 +3822,43 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-cassandra-driver/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-cassandra-driver/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-connect": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.45.0.tgz", - "integrity": "sha512-OHdp71gsRnm0lVD7SEtYSJFfvq4r6QN/5lgRK+Vrife1DHy+Insm66JJZN2Frt1waIzmDNn3VLCCafTnItfVcA==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.47.0.tgz", + "integrity": "sha512-pjenvjR6+PMRb6/4X85L4OtkQCootgb/Jzh/l/Utu3SJHBid1F+gk9sTGU2FWuhhEfV6P7MZ7BmCdHXQjgJ42g==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.38" }, @@ -3666,13 +3869,42 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-cucumber": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.16.0.tgz", - "integrity": "sha512-bLKOQFgKimQkD8th+y0zMD9vNBjq79BWmPd7QqOGV2atQFbb2QJnorp/Y6poTVQNiITv0GE2mmmcqbjF+Y+JQA==", + "node_modules/@opentelemetry/instrumentation-connect/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-connect/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-cucumber": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.18.0.tgz", + "integrity": "sha512-i+cUbLHvRShuevtM0NwjQR9wnABhmYw8+dbgD57LNBde7xkuSDot0CTzX+pYn32djtQ1bPYZiLf+uwS0JsMUrw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3682,13 +3914,71 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/instrumentation-dataloader": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.18.0.tgz", - "integrity": "sha512-egPb8OcGZP6GUU/dbB8NnVgnSIqlM0nHS8KkADq51rVaMkzBcevtinYDFYTQu9tuQ6GEwaSdiQxiQORpYaVeQw==", + "node_modules/@opentelemetry/instrumentation-cucumber/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0" + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-cucumber/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.21.0.tgz", + "integrity": "sha512-Xu4CZ1bfhdkV3G6iVHFgKTgHx8GbKSqrTU01kcIJRGHpowVnyOPEv1CW5ow+9GU2X4Eki8zoNuVUenFc3RluxQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dataloader/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dataloader/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3698,12 +3988,41 @@ } }, "node_modules/@opentelemetry/instrumentation-dns": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.45.0.tgz", - "integrity": "sha512-gE02Jj97aaYUdZIvp2RwWPy3DLN86k15YvPRzkMaPWZKVwsKrHcA+xVX8k3rh9o0g64PC/U2f+LXiJr14PyVLg==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.47.0.tgz", + "integrity": "sha512-775fOnewWkTF4iXMGKgwvOGqEmPrU1PZpXjjqvTrEErYBJe7Fz1WlEeUStHepyKOdld7Ghv7TOF/kE3QDctvrg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0" + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dns/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dns/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3713,13 +4032,13 @@ } }, "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.50.0.tgz", - "integrity": "sha512-0VF7HM8hTe0B5oXqCfBljMYFeQ3WKKqs0kCTRT02/Pjnmj5bOmR62r2dstjxbxnGKoeFRUHD/QAown9gyf659A==", + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.52.0.tgz", + "integrity": "sha512-W7pizN0Wh1/cbNhhTf7C62NpyYw7VfCFTYg0DYieSTrtPBT1vmoSZei19wfKLnrMsz3sHayCg0HxCVL2c+cz5w==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3729,14 +4048,43 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-express/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-fastify": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.46.0.tgz", - "integrity": "sha512-tib8SH5RCqhYRw9Qcpep9tP6ABxyXFDljdRy2aKpklHaFAyDELr3EpEAkGdkMZtO5Y3/QhUsmzYZp1np9jkjUg==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.48.0.tgz", + "integrity": "sha512-3zQlE/DoVfVH6/ycuTv7vtR/xib6WOa0aLFfslYcvE62z0htRu/ot8PV/zmMZfnzpTQj8S/4ULv36R6UIbpJIg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3746,14 +4094,72 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-fastify/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fastify/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-fs": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.21.0.tgz", - "integrity": "sha512-p2Fn78KSSbSSIJOOTn9FbxEzNRIIsYn9KTemKhABuunVqHixIqQ3hUjChbR+RbjPNZQthDC/0GHDeihRoyLdLQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.23.0.tgz", + "integrity": "sha512-Puan+QopWHA/KNYvDfOZN6M/JtF6buXEyD934vrb8WhsX1/FuM7OtoMlQyIqAadnE8FqqDL4KDPiEfCQH6pQcQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0" + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fs/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fs/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3763,12 +4169,41 @@ } }, "node_modules/@opentelemetry/instrumentation-generic-pool": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.45.0.tgz", - "integrity": "sha512-+fk7tnpzkkBAQzEtyJA0zRv7aBDhr05zczyBn//iJdmDG+ZfQFuIKK4dXNnv9FUZpedW0wcHlPqbP5FIGhAsLQ==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.47.0.tgz", + "integrity": "sha512-UfHqf3zYK+CwDwEtTjaD12uUqGGTswZ7ofLBEdQ4sEJp9GHSSJMQ2hT3pgBxyKADzUdoxQAv/7NqvL42ZI+Qbw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0" + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-generic-pool/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-generic-pool/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3778,12 +4213,41 @@ } }, "node_modules/@opentelemetry/instrumentation-graphql": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.49.0.tgz", - "integrity": "sha512-FZaOS/BmE5npzk95X3Iqfo80a6wEJlkAtk7wLUJG/VZaB8RbBjJow4g0YdtvK8GNGEQW02KiQ+VtzdPGRemlwg==", + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.51.0.tgz", + "integrity": "sha512-LchkOu9X5DrXAnPI1+Z06h/EH/zC7D6sA86hhPrk3evLlsJTz0grPrkL/yUJM9Ty0CL/y2HSvmWQCjbJEz/ADg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0" + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-graphql/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-graphql/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3793,12 +4257,12 @@ } }, "node_modules/@opentelemetry/instrumentation-grpc": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.201.1.tgz", - "integrity": "sha512-OIkXkVnilh8E6YKz/PiQtWeERqbcbjtVppMc7A2h39eaoaKnckXxom3YXhX+/PMhfmjbUnqw6k/KvmUr9zig1Q==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.203.0.tgz", + "integrity": "sha512-Qmjx2iwccHYRLoE4RFS46CvQE9JG9Pfeae4EPaNZjvIuJxb/pZa2R9VWzRlTehqQWpAvto/dGhtkw8Tv+o0LTg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "0.201.1", + "@opentelemetry/instrumentation": "0.203.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3808,14 +4272,43 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-grpc/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-grpc/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-hapi": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.47.0.tgz", - "integrity": "sha512-0BCiQl2+oAuhSzbZrgpZgRvg7PclTfb7GxuBqWmWj9XkRk6cKla18S0pBqRCtl+qluRIaZ7tyXKmdtlsXj0QIw==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.50.0.tgz", + "integrity": "sha512-5xGusXOFQXKacrZmDbpHQzqYD1gIkrMWuwvlrEPkYOsjUqGUjl1HbxCsn5Y9bUXOCgP1Lj6A4PcKt1UiJ2MujA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3825,14 +4318,43 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-hapi/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-hapi/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.201.1.tgz", - "integrity": "sha512-xhkL/eOntScSLS8C2/LHKZ9Z9MEyGB9Yil7lF3JV0+YBeLXHQUIw2xPD7T0qw0DnqlrN8c/gi8hb5BEXZcyHRg==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.203.0.tgz", + "integrity": "sha512-y3uQAcCOAwnO6vEuNVocmpVzG3PER6/YZqbPbbffDdJ9te5NkHEkfSMNzlC3+v7KlE+WinPGc3N7MR30G1HY2g==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", - "@opentelemetry/instrumentation": "0.201.1", + "@opentelemetry/instrumentation": "0.203.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, @@ -3843,14 +4365,43 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-ioredis": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.49.0.tgz", - "integrity": "sha512-CcbA9ylntqK7/lo7NUD/I+Uj6xcIiFFk1O2RnY23MugJunqZIFufvYkdh1mdG2bvBKdIVvA2nkVVt1Igw0uw1A==", + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", - "@opentelemetry/redis-common": "^0.37.0", + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.51.0.tgz", + "integrity": "sha512-9IUws0XWCb80NovS+17eONXsw1ZJbHwYYMXiwsfR9TSurkLV5UNbRSKb9URHO+K+pIJILy9wCxvyiOneMr91Ig==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/redis-common": "^0.38.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3860,13 +4411,51 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-kafkajs": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.10.0.tgz", - "integrity": "sha512-0roBjhMaW5li1gXVqrBRjzeLPWUiym8TPQi3iXqMA3GizPzilE4hwhIVI7GxtMHAdS15TgkUce6WVYVOBFrrbg==", + "node_modules/@opentelemetry/instrumentation-ioredis/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-ioredis/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-ioredis/node_modules/@opentelemetry/redis-common": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.0.tgz", + "integrity": "sha512-4Wc0AWURII2cfXVVoZ6vDqK+s5n4K5IssdrlVrvGsx6OEOKdghKtJZqXAHWFiZv4nTDLH2/2fldjIHY8clMOjQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.12.0.tgz", + "integrity": "sha512-bIe4aSAAxytp88nzBstgr6M7ZiEpW6/D1/SuKXdxxuprf18taVvFL2H5BDNGZ7A14K27haHqzYqtCTqFXHZOYg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.30.0" }, "engines": { @@ -3876,14 +4465,72 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.46.0.tgz", - "integrity": "sha512-+AxDwDdLJB467mEPOQKHod/1NDzX8msUAOEiViMkM7xAJoUsHTrP6EKlbjrCKkK+X2Eqh2pTO0ibeLkhG96oNA==", + "node_modules/@opentelemetry/instrumentation-kafkajs/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.48.0.tgz", + "integrity": "sha512-V5wuaBPv/lwGxuHjC6Na2JFRjtPgstw19jTFl1B1b6zvaX8zVDYUDaR5hL7glnQtUSCMktPttQsgK4dhXpddcA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.33.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-knex/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-knex/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3893,13 +4540,13 @@ } }, "node_modules/@opentelemetry/instrumentation-koa": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.49.0.tgz", - "integrity": "sha512-LO2pdZ5SF2LzWZLwrPTja/sQN8Kl4Wu5QvWSFJJLLGpeVKQWC4n41qjPUAAu668w43s42xqfs9bC4hWmQe7o8g==", + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.51.0.tgz", + "integrity": "sha512-XNLWeMTMG1/EkQBbgPYzCeBD0cwOrfnn8ao4hWgLv0fNCFQu1kCsJYygz2cvKuCs340RlnG4i321hX7R8gj3Rg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3909,13 +4556,71 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-lru-memoizer": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.46.0.tgz", - "integrity": "sha512-k8wdehAJYuSYWKiIDXrXSd7+33M4qOUEhrE3ymNFOHxVjwtUWpSh6JYSFe+5pqGilhl4CqUgxCkaQ9kPy3rAOQ==", + "node_modules/@opentelemetry/instrumentation-koa/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0" + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-koa/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.48.0.tgz", + "integrity": "sha512-KUW29wfMlTPX1wFz+NNrmE7IzN7NWZDrmFWHM/VJcmFEuQGnnBuTIdsP55CnBDxKgQ/qqYFp4udQFNtjeFosPw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3925,12 +4630,12 @@ } }, "node_modules/@opentelemetry/instrumentation-memcached": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.45.0.tgz", - "integrity": "sha512-9NjbvCBM7p+wh/sHfSGDvrtinFYqIr6qunL9nN3e86eIQh3WyE9YdnlFGRbBR+MOzTCwSzrKAvY+J0fQe91VHA==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.47.0.tgz", + "integrity": "sha512-vXDs/l4hlWy1IepPG1S6aYiIZn+tZDI24kAzwKKJmR2QEJRL84PojmALAEJGazIOLl/VdcCPZdMb0U2K0VzojA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/memcached": "^2.2.6" }, @@ -3941,13 +4646,42 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.54.0.tgz", - "integrity": "sha512-xTECmvFNfavpNz7btxmmvkCZKdHphQSSf0J4tSw4OOT0CSTythB/IWo41mYBd6GIutkmeA12dkKPd8zAU7zzyA==", + "node_modules/@opentelemetry/instrumentation-memcached/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-memcached/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.56.0.tgz", + "integrity": "sha512-YG5IXUUmxX3Md2buVMvxm9NWlKADrnavI36hbJsihqqvBGsWnIfguf0rUP5Srr0pfPqhQjUP+agLMsvu0GmUpA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3957,14 +4691,43 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-mongodb/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-mongoose": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.48.0.tgz", - "integrity": "sha512-kvopwp/kb1wN8jd0HhIBx/ZxbSmwqhN7LLvl9a7fXYACYlewUtCnVJLG80kwuG+rexRZlxeDfjoacFRDQSf9XA==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.50.0.tgz", + "integrity": "sha512-Am8pk1Ct951r4qCiqkBcGmPIgGhoDiFcRtqPSLbJrUZqEPUsigjtMjoWDRLG1Ki1NHgOF7D0H7d+suWz1AAizw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3974,15 +4737,73 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-mysql": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.47.0.tgz", - "integrity": "sha512-QWJNDNW0JyHj3cGtQOeNBcrDeOY35yX/JnDg8jEvxzmoEABHyj0EqI8fHPdOQmdctTjKTjzbqwtuAzLYIfkdAA==", + "node_modules/@opentelemetry/instrumentation-mongoose/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongoose/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.49.0.tgz", + "integrity": "sha512-QU9IUNqNsrlfE3dJkZnFHqLjlndiU39ll/YAAEvWE40sGOCi9AtOF6rmEGzJ1IswoZ3oyePV7q2MP8SrhJfVAA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/mysql": "2.15.26" + "@types/mysql": "2.15.27" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3992,12 +4813,12 @@ } }, "node_modules/@opentelemetry/instrumentation-mysql2": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.47.0.tgz", - "integrity": "sha512-rVKuKJ6HFVTNXNo8WuC3lBL/9zQ0OZfga/2dLseg/jlQZzUlWijsA57trnA92pcYxs32HBPSfKpuA88ZAVBFpA==", + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.49.0.tgz", + "integrity": "sha512-dCub9wc02mkJWNyHdVEZ7dvRzy295SmNJa+LrAJY2a/+tIiVBQqEAajFzKwp9zegVVnel9L+WORu34rGLQDzxA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.41.0" }, @@ -4008,13 +4829,42 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-nestjs-core": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.47.0.tgz", - "integrity": "sha512-xTtWbqdvlxRfhYidLEq0XvQUGqqgT4Fom21nxJ7XYvOoUJ4KNOxFBnfGW9RcXtFHDkux6rIjNP5CiPCYMZ007g==", + "node_modules/@opentelemetry/instrumentation-mysql2/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql2/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-nestjs-core": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.49.0.tgz", + "integrity": "sha512-1R/JFwdmZIk3T/cPOCkVvFQeKYzbbUvDxVH3ShXamUwBlGkdEu5QJitlRMyVNZaHkKZKWgYrBarGQsqcboYgaw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.30.0" }, "engines": { @@ -4024,13 +4874,42 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-net": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.45.0.tgz", - "integrity": "sha512-kFdY4IMth8obBPXoAlpLkea7l85Joe+p7oep+BexrHQ0iX+0cvnfoYBMMSE/vAp6T1N3Nu6RDT2Wzf3mqkHxjw==", + "node_modules/@opentelemetry/instrumentation-nestjs-core/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-nestjs-core/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-net": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.47.0.tgz", + "integrity": "sha512-csoJ++Njpf7C09JH+0HNGenuNbDZBqO1rFhMRo6s0rAmJwNh9zY3M/urzptmKlqbKnf4eH0s+CKHy/+M8fbFsQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -4040,13 +4919,42 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-oracledb": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-oracledb/-/instrumentation-oracledb-0.27.0.tgz", - "integrity": "sha512-b/JBJroC22DqgeMUSLYyleN6ohyXbCK1YGvBsCuDdiYUmOOyyWYSKdm4D26hTwFv1TKce+Im6aGcXF1hq2WKuQ==", + "node_modules/@opentelemetry/instrumentation-net/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-net/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-oracledb": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-oracledb/-/instrumentation-oracledb-0.29.0.tgz", + "integrity": "sha512-2aHLiJdkyiUbooIUm7FaZf+O4jyqEl+RfFpgud1dxT87QeeYM216wi+xaMNzsb5yKtRBqbA3qeHBCyenYrOZwA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/oracledb": "6.5.2" }, @@ -4057,17 +4965,46 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-oracledb/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-oracledb/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-pg": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.53.0.tgz", - "integrity": "sha512-riWbJvSviTAsjeuq8fn7Y7+CXEYf3sGR18WfLeM7GgSnptTOur1++SLTN7XogqiwP3LFFQ0GLoYe+hxVOEyEpw==", + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.55.0.tgz", + "integrity": "sha512-yfJ5bYE7CnkW/uNsnrwouG/FR7nmg09zdk2MSs7k0ZOMkDDAE3WBGpVFFApGgNu2U+gtzLgEzOQG4I/X+60hXw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.41.0", - "@types/pg": "8.6.1", + "@types/pg": "8.15.4", "@types/pg-pool": "2.0.6" }, "engines": { @@ -4077,15 +5014,73 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-pino": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.48.0.tgz", - "integrity": "sha512-+X+GTaXFuExrmQ3XS1HH8E+4KkKQ1HPzjNGnckuW/SQVOxRGeZMwJu1s60lx4eLpQuXXRh9nJaCAqMi/As347w==", + "node_modules/@opentelemetry/instrumentation-pg/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.201.0", + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pino": { + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.50.0.tgz", + "integrity": "sha512-Pi0cWGp4f2gresq2xqef4IsuunLdebJ9n9tZxytDz2ci4euIfW36ILpszQmRNhwCVDCZLmUgGDKZGj4PXyPd0w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.203.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0" + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pino/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pino/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4095,13 +5090,13 @@ } }, "node_modules/@opentelemetry/instrumentation-redis": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.48.0.tgz", - "integrity": "sha512-bp82CqAcBNk0+nneAX2L+wbCKiNHTnTEJlppOEjxESIR8AocSKO7gnWpotTh5Bki2UULUn62MBXJmRnIzj0ikw==", + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.51.0.tgz", + "integrity": "sha512-uL/GtBA0u72YPPehwOvthAe+Wf8k3T+XQPBssJmTYl6fzuZjNq8zTfxVFhl9nRFjFVEe+CtiYNT0Q3AyqW1Z0A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", - "@opentelemetry/redis-common": "^0.37.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/redis-common": "^0.38.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -4111,15 +5106,27 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-redis-4": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.48.0.tgz", - "integrity": "sha512-aHZGrVwOsCM5u2PQdK1/PJuIWjGjYhOKEqqaPg3Mere2C6brwp+ih1bjcGyMRBS+7KNn5OSPcsFWpcW17Bfotw==", + "node_modules/@opentelemetry/instrumentation-redis/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", - "@opentelemetry/redis-common": "^0.37.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4128,14 +5135,23 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-redis/node_modules/@opentelemetry/redis-common": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.0.tgz", + "integrity": "sha512-4Wc0AWURII2cfXVVoZ6vDqK+s5n4K5IssdrlVrvGsx6OEOKdghKtJZqXAHWFiZv4nTDLH2/2fldjIHY8clMOjQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, "node_modules/@opentelemetry/instrumentation-restify": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.47.0.tgz", - "integrity": "sha512-A1VixeXnRAQQfWidjnNqOwqGp1K5/r6fIyCdL+1Yvde11HiruMQOf6B71D7wWJHRtNKpLhq3o8JzeNGJoBEMpA==", + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.49.0.tgz", + "integrity": "sha512-tsGZZhS4mVZH7omYxw5jpsrD3LhWizqWc0PYtAnzpFUvL5ZINHE+cm57bssTQ2AK/GtZMxu9LktwCvIIf3dSmw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -4145,13 +5161,42 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-restify/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-restify/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-router": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.46.0.tgz", - "integrity": "sha512-p98dJcw0reSyfkhRwzx8HrhyjcKmyguIE0KCLcxBnvQFnPL7EfUR2up2M9ggceFiZO5GUo1gk+r/mP+B9VBsQw==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.48.0.tgz", + "integrity": "sha512-Wixrc8CchuJojXpaS/dCQjFOMc+3OEil1H21G+WLYQb8PcKt5kzW9zDBT19nyjjQOx/D/uHPfgbrT+Dc7cfJ9w==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -4161,13 +5206,71 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-router/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-router/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-runtime-node": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.15.0.tgz", - "integrity": "sha512-K3aPMYImALNsovPUjlIHctS2oH1YESlIAQMgiHXvcUxxz6+d66pPE1a4IoGP19iFOmRDMjshgHR/0DXMOEvZKg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.17.0.tgz", + "integrity": "sha512-O+xc0woqrSjue5IgpCCMvlgsuDrq6DDEfiHW3S3vRMCjXE1ZoPjaDE/K6EURorN+tjnzZQN1gOMSrscSGAbjHg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0" + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4177,12 +5280,12 @@ } }, "node_modules/@opentelemetry/instrumentation-socket.io": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.48.0.tgz", - "integrity": "sha512-bVFiRvQnAW9hT+8FZVuhhybAvopAShLGm6LYz8raNZokxEw2FzGDVXONWaAM5D2/RbCbMl7R+PLN//3SEU/k0g==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.50.0.tgz", + "integrity": "sha512-6JN6lnKN9ZuZtZdMQIR+no1qHzQvXSZUsNe3sSWMgqmNRyEXuDUWBIyKKeG0oHRHtR4xE4QhJyD4D5kKRPWZFA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -4192,13 +5295,42 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.20.0.tgz", - "integrity": "sha512-8OqIj554Rh8sll9myfDaFD1cYY8XKpxK3SMzCTZGc4BqS61gU0kd7UEydZeplrkQHDgySP4nvtFfkQCaZyTS4Q==", + "node_modules/@opentelemetry/instrumentation-socket.io/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-socket.io/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.22.0.tgz", + "integrity": "sha512-XrrNSUCyEjH1ax9t+Uo6lv0S2FCCykcF7hSxBMxKf7Xn0bPRxD3KyFUZy25aQXzbbbUHhtdxj3r2h88SfEM3aA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/tedious": "^4.0.14" }, @@ -4209,14 +5341,43 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-tedious/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-tedious/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.12.0.tgz", - "integrity": "sha512-SLqTWPWWwqSZVYZw3a9sdcNXsahJfimvDpYaoDd6ryvQGDlOrHVKr56gL5qD3XDVa67DmV5ZQrxRrnYUdlp3BQ==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.14.0.tgz", + "integrity": "sha512-2HN+7ztxAReXuxzrtA3WboAKlfP5OsPA57KQn2AdYZbJ3zeRPcLXyW4uO/jpLE6PLm0QRtmeGCmfYpqRlwgSwg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.201.0" + "@opentelemetry/instrumentation": "^0.203.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4225,14 +5386,72 @@ "@opentelemetry/api": "^1.7.0" } }, - "node_modules/@opentelemetry/instrumentation-winston": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.46.0.tgz", - "integrity": "sha512-/nvmsLSON9Ki8C32kOMAkzsCpFfpjI2Fvr51uAY8/8bwG258MUUN8fCbAOMaiaPEKiB807wsE/aym83LYiB0ng==", + "node_modules/@opentelemetry/instrumentation-undici/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.201.0", - "@opentelemetry/instrumentation": "^0.201.0" + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-undici/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-winston": { + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.48.0.tgz", + "integrity": "sha512-QuKbswAaQfRULhtlYbeNC9gOAXPxOSCE4BjIzuY1oEsc84kIsHUjn3yvY9Q83s3eg3j0JycNcAMi8u0yTl5PIQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.203.0", + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-winston/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-winston/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4242,13 +5461,13 @@ } }, "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.201.1.tgz", - "integrity": "sha512-FiS/mIWmZXyRxYGyXPHY+I/4+XrYVTD7Fz/zwOHkVPQsA1JTakAOP9fAi6trXMio0dIpzvQujLNiBqGM7ExrQw==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.203.0.tgz", + "integrity": "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", - "@opentelemetry/otlp-transformer": "0.201.1" + "@opentelemetry/otlp-transformer": "0.203.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4258,15 +5477,15 @@ } }, "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.201.1.tgz", - "integrity": "sha512-Y0h9hiMvNtUuXUMkYNAt81hxnFuOHHSeu/RC+pXcHe7S6ac0ROlcjdabBKmYSadJxRrP4YfLahLRuNkVtZow4w==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.203.0.tgz", + "integrity": "sha512-te0Ze1ueJF+N/UOFl5jElJW4U0pZXQ8QklgSfJ2linHN0JJsuaHG8IabEUi2iqxY8ZBDlSiz1Trfv5JcjWWWwQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.1", - "@opentelemetry/otlp-exporter-base": "0.201.1", - "@opentelemetry/otlp-transformer": "0.201.1" + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4276,15 +5495,15 @@ } }, "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.201.1.tgz", - "integrity": "sha512-+q/8Yuhtu9QxCcjEAXEO8fXLjlSnrnVwfzi9jiWaMAppQp69MoagHHomQj02V2WnGjvBod5ajgkbK4IoWab50A==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.203.0.tgz", + "integrity": "sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.201.1", + "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", - "@opentelemetry/sdk-logs": "0.201.1", + "@opentelemetry/sdk-logs": "0.203.0", "@opentelemetry/sdk-metrics": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1", "protobufjs": "^7.3.0" @@ -4296,10 +5515,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/propagation-utils": { - "version": "0.31.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.31.1.tgz", - "integrity": "sha512-YLNt7SWy4HZwI9d+4+OevQs2Gmof27TkjR3v029UGw8zFOcyONyIQhHHx7doyRbrLpWZtUc91cnCA4mKhArCXw==", + "version": "0.31.3", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.31.3.tgz", + "integrity": "sha512-ZI6LKjyo+QYYZY5SO8vfoCQ9A69r1/g+pyjvtu5RSK38npINN1evEmwqbqhbg2CdcIK3a4PN6pDAJz/yC5/gAA==", "license": "Apache-2.0", "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4338,19 +5569,10 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/redis-common": { - "version": "0.37.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.37.0.tgz", - "integrity": "sha512-tJwgE6jt32bLs/9J6jhQRKU2EZnsD8qaO13aoFyXwF6s4LhpT7YFHf3Z03MqdILk6BA2BFUhoyh7k9fj9i032A==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - } - }, "node_modules/@opentelemetry/resource-detector-alibaba-cloud": { - "version": "0.31.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.31.1.tgz", - "integrity": "sha512-RPitvB5oHZsECnK7xtUAFdyBXRdtJbY0eEzQPBrLMQv4l/FN4pETijqv6LcKBbn6tevaoBU2bqOGnVoL4uX4Tg==", + "version": "0.31.3", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.31.3.tgz", + "integrity": "sha512-I556LHcLVsBXEgnbPgQISP/JezDt5OfpgOaJNR1iVJl202r+K145OSSOxnH5YOc/KvrydBD0FOE03F7x0xnVTw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", @@ -4365,9 +5587,9 @@ } }, "node_modules/@opentelemetry/resource-detector-aws": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-2.1.0.tgz", - "integrity": "sha512-7QG5wQXMiHseKIyU69m8vfZgLhrxFx48DdyaQEYj6GXjE/Xrv1nS3bUwhICjb6+4NorB9+1pFCvJ/4S01CCCjQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-2.3.0.tgz", + "integrity": "sha512-PkD/lyXG3B3REq1Y6imBLckljkJYXavtqGYSryAeJYvGOf5Ds3doR+BCGjmKeF6ObAtI5MtpBeUStTDtGtBsWA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", @@ -4382,9 +5604,9 @@ } }, "node_modules/@opentelemetry/resource-detector-azure": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.8.0.tgz", - "integrity": "sha512-YBsJQrt0NGT66BgdVhhTkv7/oe/rTflX/rKteptVK6HNo7z8wbeAbB4SnSNJFfF+v3XrP/ruiTxKnNzoh/ampw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.10.0.tgz", + "integrity": "sha512-5cNAiyPBg53Uxe/CW7hsCq8HiKNAUGH+gi65TtgpzSR9bhJG4AEbuZhbJDFwe97tn2ifAD1JTkbc/OFuaaFWbA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", @@ -4399,9 +5621,9 @@ } }, "node_modules/@opentelemetry/resource-detector-container": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.7.1.tgz", - "integrity": "sha512-I2vXgdA8mhIlAktIp7NovicalqKPaas9APH5wQxIzMK6jPjZmwS5x0MBW+sTsaFM4pnOf/Md9enoDnnR5CLq5A==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.7.3.tgz", + "integrity": "sha512-SK+xUFw6DKYbQniaGmIFsFxAZsr8RpRSRWxKi5/ZJAoqqPnjcyGI/SeUx8zzPk4XLO084zyM4pRHgir0hRTaSQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", @@ -4416,9 +5638,9 @@ } }, "node_modules/@opentelemetry/resource-detector-gcp": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.35.0.tgz", - "integrity": "sha512-JYkyOUc7TZAyHy37N2aPAwFvRdET0+E5qIRjmQLPop9LQi4+N0sKf65g4xCwuY/0M721T/424G3zneJjxyiooA==", + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.37.0.tgz", + "integrity": "sha512-LGpJBECIMsVKhiulb4nxUw++m1oF4EiDDPmFGW2aqYaAF0oUvJNv8Z/55CAzcZ7SxvlTgUwzewXDBsuCup7iqw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", @@ -4450,12 +5672,12 @@ } }, "node_modules/@opentelemetry/sdk-logs": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.201.1.tgz", - "integrity": "sha512-Ug8gtpssUNUnfpotB9ZhnSsPSGDu+7LngTMgKl31mmVJwLAKyl6jC8diZrMcGkSgBh0o5dbg9puvLyR25buZfw==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.203.0.tgz", + "integrity": "sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.201.1", + "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" }, @@ -4466,6 +5688,18 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/sdk-metrics": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", @@ -4483,29 +5717,29 @@ } }, "node_modules/@opentelemetry/sdk-node": { - "version": "0.201.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.201.1.tgz", - "integrity": "sha512-OdkYe6ZEFbPq+YXhebuiYpPECIBrrKgFJoAQVATllKlB5RDQDTE4J84/8LwGfQqSxBiSK2u1aSaFpzgBVoBrKA==", + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.203.0.tgz", + "integrity": "sha512-zRMvrZGhGVMvAbbjiNQW3eKzW/073dlrSiAKPVWmkoQzah9wfynpVPeL55f9fVIm0GaBxTLcPeukWGy0/Wj7KQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.201.1", + "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", - "@opentelemetry/exporter-logs-otlp-grpc": "0.201.1", - "@opentelemetry/exporter-logs-otlp-http": "0.201.1", - "@opentelemetry/exporter-logs-otlp-proto": "0.201.1", - "@opentelemetry/exporter-metrics-otlp-grpc": "0.201.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.201.1", - "@opentelemetry/exporter-metrics-otlp-proto": "0.201.1", - "@opentelemetry/exporter-prometheus": "0.201.1", - "@opentelemetry/exporter-trace-otlp-grpc": "0.201.1", - "@opentelemetry/exporter-trace-otlp-http": "0.201.1", - "@opentelemetry/exporter-trace-otlp-proto": "0.201.1", + "@opentelemetry/exporter-logs-otlp-grpc": "0.203.0", + "@opentelemetry/exporter-logs-otlp-http": "0.203.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.203.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.203.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.203.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.203.0", + "@opentelemetry/exporter-prometheus": "0.203.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.203.0", + "@opentelemetry/exporter-trace-otlp-http": "0.203.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.203.0", "@opentelemetry/exporter-zipkin": "2.0.1", - "@opentelemetry/instrumentation": "0.201.1", + "@opentelemetry/instrumentation": "0.203.0", "@opentelemetry/propagator-b3": "2.0.1", "@opentelemetry/propagator-jaeger": "2.0.1", "@opentelemetry/resources": "2.0.1", - "@opentelemetry/sdk-logs": "0.201.1", + "@opentelemetry/sdk-logs": "0.203.0", "@opentelemetry/sdk-metrics": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1", "@opentelemetry/sdk-trace-node": "2.0.1", @@ -4518,6 +5752,35 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz", @@ -4553,9 +5816,9 @@ } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz", - "integrity": "sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ==", + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.36.0.tgz", + "integrity": "sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==", "license": "Apache-2.0", "engines": { "node": ">=14" @@ -4603,9 +5866,9 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", - "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", "dev": true, "license": "MIT", "engines": { @@ -4695,9 +5958,9 @@ } }, "node_modules/@react-email/button": { - "version": "0.0.19", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.19.tgz", - "integrity": "sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.0.tgz", + "integrity": "sha512-8i+v6cMxr2emz4ihCrRiYJPp2/sdYsNNsBzXStlcA+/B9Umpm5Jj3WJKYpgTPM+aeyiqlG/MMI1AucnBm4f1oQ==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -4707,9 +5970,9 @@ } }, "node_modules/@react-email/code-block": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.13.tgz", - "integrity": "sha512-4DE4yPSgKEOnZMzcrDvRuD6mxsNxOex0hCYEG9F9q23geYgb2WCCeGBvIUXVzK69l703Dg4Vzrd5qUjl+JfcwA==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.1.0.tgz", + "integrity": "sha512-jSpHFsgqnQXxDIssE4gvmdtFncaFQz5D6e22BnVjcCPk/udK+0A9jRwGFEG8JD2si9ZXBmU4WsuqQEczuZn4ww==", "license": "MIT", "dependencies": { "prismjs": "^1.30.0" @@ -4746,14 +6009,14 @@ } }, "node_modules/@react-email/components": { - "version": "0.0.41", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.41.tgz", - "integrity": "sha512-WUI3wHwra3QS0pwrovSU6b0I0f3TvY33ph0y44LuhSYDSQlMRyeOzgoT6HRDY5FXMDF57cHYq9WoKwpwP0yd7Q==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.3.2.tgz", + "integrity": "sha512-nVbo0KtBdZbj19lvfFpe0ZhjKPh6LE229+NyQLuTDt6dfaLzNRpSu/rHP+jlvdWBAk93slsoGyWDRldbqklpaA==", "license": "MIT", "dependencies": { "@react-email/body": "0.0.11", - "@react-email/button": "0.0.19", - "@react-email/code-block": "0.0.13", + "@react-email/button": "0.2.0", + "@react-email/code-block": "0.1.0", "@react-email/code-inline": "0.0.5", "@react-email/column": "0.0.13", "@react-email/container": "0.0.15", @@ -4766,11 +6029,11 @@ "@react-email/link": "0.0.12", "@react-email/markdown": "0.0.15", "@react-email/preview": "0.0.13", - "@react-email/render": "1.1.2", + "@react-email/render": "1.1.3", "@react-email/row": "0.0.12", "@react-email/section": "0.0.16", - "@react-email/tailwind": "1.0.5", - "@react-email/text": "0.1.4" + "@react-email/tailwind": "1.2.2", + "@react-email/text": "0.1.5" }, "engines": { "node": ">=18.0.0" @@ -4900,9 +6163,9 @@ } }, "node_modules/@react-email/render": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.1.2.tgz", - "integrity": "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.1.3.tgz", + "integrity": "sha512-TjjF1tdTmOqYEIWWg9wMx5q9JbQRbWmnG7owQbSGEHkNfc/c/vBu7hjfrki907lgQEAkYac9KPTyIjOKhvhJCg==", "license": "MIT", "dependencies": { "html-to-text": "^9.0.5", @@ -4942,9 +6205,9 @@ } }, "node_modules/@react-email/tailwind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.0.5.tgz", - "integrity": "sha512-BH00cZSeFfP9HiDASl+sPHi7Hh77W5nzDgdnxtsVr/m3uQD9g180UwxcE3PhOfx0vRdLzQUU8PtmvvDfbztKQg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.2.2.tgz", + "integrity": "sha512-heO9Khaqxm6Ulm6p7HQ9h01oiiLRrZuuEQuYds/O7Iyp3c58sMVHZGIxiRXO/kSs857NZQycpjewEVKF3jhNTw==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -4954,9 +6217,9 @@ } }, "node_modules/@react-email/text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.4.tgz", - "integrity": "sha512-cMNE02y8172DocpNGh97uV5HSTawaS4CKG/zOku8Pu+m6ehBKbAjgtQZDIxhgstw8+TWraFB8ltS1DPjfG8nLA==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.5.tgz", + "integrity": "sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -4966,9 +6229,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -4988,10 +6251,227 @@ } } }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz", + "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz", + "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", + "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", + "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz", + "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz", + "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz", + "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz", + "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", + "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", + "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz", + "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz", + "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz", + "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz", + "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz", + "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", - "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", + "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", "cpu": [ "x64" ], @@ -5003,9 +6483,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", - "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", + "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", "cpu": [ "x64" ], @@ -5016,6 +6496,48 @@ "linux" ] }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", + "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz", + "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", + "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -5083,15 +6605,15 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.11.29", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.29.tgz", - "integrity": "sha512-g4mThMIpWbNhV8G2rWp5a5/Igv8/2UFRJx2yImrLGMgrDDYZIopqZ/z0jZxDgqNA1QDx93rpwNF7jGsxVWcMlA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.0.tgz", + "integrity": "sha512-7Fh16ZH/Rj3Di720if+sw9BictD4N5kbTpsyDC+URXhvsZ7qRt1lH7PaeIQYyJJQHwFhoKpwwGxfGU9SHgPLdw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.21" + "@swc/types": "^0.1.23" }, "engines": { "node": ">=10" @@ -5101,16 +6623,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.29", - "@swc/core-darwin-x64": "1.11.29", - "@swc/core-linux-arm-gnueabihf": "1.11.29", - "@swc/core-linux-arm64-gnu": "1.11.29", - "@swc/core-linux-arm64-musl": "1.11.29", - "@swc/core-linux-x64-gnu": "1.11.29", - "@swc/core-linux-x64-musl": "1.11.29", - "@swc/core-win32-arm64-msvc": "1.11.29", - "@swc/core-win32-ia32-msvc": "1.11.29", - "@swc/core-win32-x64-msvc": "1.11.29" + "@swc/core-darwin-arm64": "1.13.0", + "@swc/core-darwin-x64": "1.13.0", + "@swc/core-linux-arm-gnueabihf": "1.13.0", + "@swc/core-linux-arm64-gnu": "1.13.0", + "@swc/core-linux-arm64-musl": "1.13.0", + "@swc/core-linux-x64-gnu": "1.13.0", + "@swc/core-linux-x64-musl": "1.13.0", + "@swc/core-win32-arm64-msvc": "1.13.0", + "@swc/core-win32-ia32-msvc": "1.13.0", + "@swc/core-win32-x64-msvc": "1.13.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -5122,9 +6644,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.29", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.29.tgz", - "integrity": "sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.0.tgz", + "integrity": "sha512-SkmR9u7MHDu2X8hf7SjZTmsAfQTmel0mi+TJ7AGtufLwGySv6pwQfJ/CIJpcPxYENVqDJAFnDrHaKV8mgA6kxQ==", "cpu": [ "arm64" ], @@ -5139,9 +6661,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.11.29", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.29.tgz", - "integrity": "sha512-S3eTo/KYFk+76cWJRgX30hylN5XkSmjYtCBnM4jPLYn7L6zWYEPajsFLmruQEiTEDUg0gBEWLMNyUeghtswouw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.0.tgz", + "integrity": "sha512-15/SyDjXRtFJ09fYHBXUXrj4tpiSpCkjgsF1z3/sSpHH1POWpQUQzxmFyomPQVZ/SsDqP18WGH09Vph4Qriuiw==", "cpu": [ "x64" ], @@ -5156,9 +6678,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.29", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.29.tgz", - "integrity": "sha512-o9gdshbzkUMG6azldHdmKklcfrcMx+a23d/2qHQHPDLUPAN+Trd+sDQUYArK5Fcm7TlpG4sczz95ghN0DMkM7g==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.0.tgz", + "integrity": "sha512-AHauVHZQEJI/dCZQg6VYNNQ6HROz8dSOnCSheXzzBw1DGWo77BlcxRP0fF0jaAXM9WNqtCUOY1HiJ9ohkAE61Q==", "cpu": [ "arm" ], @@ -5173,9 +6695,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.29", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.29.tgz", - "integrity": "sha512-sLoaciOgUKQF1KX9T6hPGzvhOQaJn+3DHy4LOHeXhQqvBgr+7QcZ+hl4uixPKTzxk6hy6Hb0QOvQEdBAAR1gXw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.0.tgz", + "integrity": "sha512-qyZmBZF7asF6954/x7yn6R7Bzd45KRG05rK2atIF9J3MTa8az7vubP1Q3BWmmss1j8699DELpbuoJucGuhsNXw==", "cpu": [ "arm64" ], @@ -5190,9 +6712,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.29", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.29.tgz", - "integrity": "sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.0.tgz", + "integrity": "sha512-whskQCOUlLQT7MjnronpHmyHegBka5ig9JkQvecbqhWzRfdwN+c2xTJs3kQsWy2Vc2f1hcL3D8hGIwY5TwPxMQ==", "cpu": [ "arm64" ], @@ -5207,9 +6729,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.29", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.29.tgz", - "integrity": "sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.0.tgz", + "integrity": "sha512-51n4P4nv6rblXyH3zCEktvmR9uSAZ7+zbfeby0sxbj8LS/IKuVd7iCwD5dwMj4CxG9Fs+HgjN73dLQF/OerHhg==", "cpu": [ "x64" ], @@ -5224,9 +6746,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.29", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.29.tgz", - "integrity": "sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.0.tgz", + "integrity": "sha512-VMqelgvnXs27eQyhDf1S2O2MxSdchIH7c1tkxODRtu9eotcAeniNNgqqLjZ5ML0MGeRk/WpbsAY/GWi7eSpiHw==", "cpu": [ "x64" ], @@ -5241,9 +6763,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.29", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.29.tgz", - "integrity": "sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.0.tgz", + "integrity": "sha512-NLJmseWJngWeENgat+O/WB4ptNxtx2X4OfPnSG5a/A4sxcn2E4jq91OPvbeUQwDkH+ZQWKXmbXFzt7Nn661QYA==", "cpu": [ "arm64" ], @@ -5258,9 +6780,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.29", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.29.tgz", - "integrity": "sha512-h+NjOrbqdRBYr5ItmStmQt6x3tnhqgwbj9YxdGPepbTDamFv7vFnhZR0YfB3jz3UKJ8H3uGJ65Zw1VsC+xpFkg==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.0.tgz", + "integrity": "sha512-UBfwrp0xW37KQGTA08mwrCLIm1ZKy6pXK8IVwou7BvhMgrItRNweTGyUrCnvDLUfyYFuJCmzcEaJ3NudtctD6g==", "cpu": [ "ia32" ], @@ -5275,9 +6797,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.29", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.29.tgz", - "integrity": "sha512-Q8cs2BDV9wqDvqobkXOYdC+pLUSEpX/KvI0Dgfun1F+LzuLotRFuDhrvkU9ETJA6OnD2+Fn/ieHgloiKA/Mn/g==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.0.tgz", + "integrity": "sha512-BAB1P7Z/y2EENsfsPytPnjIyBVRZN2WULY+s3ozW4QkGmYHde6XXG28n0ABTHhcIOmmR2VzM+uaW1x48laSimw==", "cpu": [ "x64" ], @@ -5295,6 +6817,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, "license": "Apache-2.0" }, "node_modules/@swc/helpers": { @@ -5310,9 +6833,9 @@ } }, "node_modules/@swc/types": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", - "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz", + "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5320,23 +6843,23 @@ } }, "node_modules/@testcontainers/postgresql": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-11.0.2.tgz", - "integrity": "sha512-Z7ZrFRAien1W1NlYxCK3PP0zUk468UHwtJubJRmktDPHTE6DLITmn+U565I7roMk66NjN3eWhly1zoUM48PtmA==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-11.2.1.tgz", + "integrity": "sha512-u0XLsjUmAHaUmB9Q1bitBu8uoxRKteDI65S5/zpJ6TeZabx9qB4EENwKqzuqEwOCzzlko9at7ZY4frwRcchgvA==", "dev": true, "license": "MIT", "dependencies": { - "testcontainers": "^11.0.2" + "testcontainers": "^11.2.1" } }, "node_modules/@testcontainers/redis": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/@testcontainers/redis/-/redis-11.0.2.tgz", - "integrity": "sha512-hLaTvjv74HtAWZFz2U/GJc5cYCIyB37TR3yCBjtBZ+dp3wZ0gc1PtsKs6qMw4fOfUVPzFm1f1pj32DF55F1+KQ==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@testcontainers/redis/-/redis-11.2.1.tgz", + "integrity": "sha512-Q5j+irNw0BLec3he30s2E0fhE06Zr9ROVutkyKUgcwQoZxEVW3xV69ke2AFCT5teEcIvTKqevObN4UDkq33Qow==", "dev": true, "license": "MIT", "dependencies": { - "testcontainers": "^11.0.2" + "testcontainers": "^11.2.1" } }, "node_modules/@tokenizer/inflate": { @@ -5424,9 +6947,9 @@ "license": "MIT" }, "node_modules/@types/aws-lambda": { - "version": "8.10.147", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.147.tgz", - "integrity": "sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==", + "version": "8.10.150", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.150.tgz", + "integrity": "sha512-AX+AbjH/rH5ezX1fbK8onC/a+HyQHo7QGmvoxAE42n22OsciAxvZoZNEr22tbXs8WfP1nIsBjKDpgPm3HjOZbA==", "license": "MIT" }, "node_modules/@types/bcrypt": { @@ -5440,9 +6963,9 @@ } }, "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, "license": "MIT", "dependencies": { @@ -5459,10 +6982,20 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/compression": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.0.tgz", - "integrity": "sha512-g4vmPIwbTii9dX1HVioHbOolubEaf4re4vDxuzpKrzz9uI7uarBExi9begX0cXyIB85jXZ5X2A/v8rsHZxSAPw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5480,9 +7013,9 @@ } }, "node_modules/@types/cookie-parser": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", - "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", + "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5497,14 +7030,21 @@ "license": "MIT" }, "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/docker-modem": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", @@ -5517,9 +7057,9 @@ } }, "node_modules/@types/dockerode": { - "version": "3.3.40", - "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.40.tgz", - "integrity": "sha512-O1ckSFYbcYv/KcnAHMLCnKQYY8/5+6CRzpsOPcQIePHRX2jG4Gmz8uXPMCXIxTGN9OYkE5eox/L67l2sGY1UYg==", + "version": "3.3.42", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.42.tgz", + "integrity": "sha512-U1jqHMShibMEWHdxYhj3rCMNCiLx5f35i4e3CEUuW+JSSszc/tVqc6WCAPdhwBymG5R/vgbcceagK0St7Cq6Eg==", "dev": true, "license": "MIT", "dependencies": { @@ -5551,16 +7091,16 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/express": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz", - "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", "dependencies": { @@ -5599,16 +7139,16 @@ "license": "MIT" }, "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "dev": true, "license": "MIT" }, "node_modules/@types/inquirer": { - "version": "8.2.10", - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.10.tgz", - "integrity": "sha512-IdD5NmHyVjWM8SHWo/kPBgtzXatwPkfwzyP3fN1jF2g9BWt5WO+8hL2F4o2GKIYsU40PpqeevuUWvkS/roXJkA==", + "version": "8.2.11", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.11.tgz", + "integrity": "sha512-15UboTvxb9SOaPG7CcXZ9dkv8lNqfiAwuh/5WxJDLjmElBt9tbx1/FDsEnJddUBKvN4mlPKvr8FyO1rAmBanzg==", "license": "MIT", "peer": true, "dependencies": { @@ -5631,16 +7171,16 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz", - "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==", + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", "dev": true, "license": "MIT" }, "node_modules/@types/luxon": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", - "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz", + "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==", "license": "MIT" }, "node_modules/@types/memcached": { @@ -5677,9 +7217,9 @@ } }, "node_modules/@types/multer": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", - "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", "dev": true, "license": "MIT", "dependencies": { @@ -5687,18 +7227,18 @@ } }, "node_modules/@types/mysql": { - "version": "2.15.26", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", - "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", + "version": "2.15.27", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", + "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "22.15.32", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz", - "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==", + "version": "22.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", + "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -5734,9 +7274,9 @@ } }, "node_modules/@types/pg": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", - "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz", + "integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -5771,9 +7311,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "dev": true, "license": "MIT" }, @@ -5785,9 +7325,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", - "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "dev": true, "license": "MIT", "dependencies": { @@ -5822,9 +7362,9 @@ "license": "MIT" }, "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", "dev": true, "license": "MIT", "dependencies": { @@ -5833,9 +7373,9 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", "dev": true, "license": "MIT", "dependencies": { @@ -5844,12 +7384,6 @@ "@types/send": "*" } }, - "node_modules/@types/shimmer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", - "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", - "license": "MIT" - }, "node_modules/@types/ssh2": { "version": "1.15.5", "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", @@ -5871,9 +7405,9 @@ } }, "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.86", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.86.tgz", - "integrity": "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ==", + "version": "18.19.112", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.112.tgz", + "integrity": "sha512-i+Vukt9POdS/MBI7YrrkkI5fMfwFtOjphSmt4WXYLfwqsfr6z/HdCx7LqT9M7JktGob8WNgj8nFB4TbGNE4Cog==", "dev": true, "license": "MIT", "dependencies": { @@ -5938,23 +7472,23 @@ "license": "MIT" }, "node_modules/@types/validator": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz", - "integrity": "sha512-nh7nrWhLr6CBq9ldtw0wx+z9wKnnv/uTVLA9g/3/TcOYxbpOSZE+MhKPmWqU+K0NvThjhv12uD8MuqijB0WzEA==", + "version": "13.15.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", + "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==", "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", + "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/type-utils": "8.37.0", + "@typescript-eslint/utils": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -5968,15 +7502,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.37.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -5984,16 +7518,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", + "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4" }, "engines": { @@ -6008,15 +7542,37 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", + "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/tsconfig-utils": "^8.37.0", + "@typescript-eslint/types": "^8.37.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", + "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6026,15 +7582,33 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", + "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", + "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -6051,9 +7625,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz", + "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==", "dev": true, "license": "MIT", "engines": { @@ -6065,14 +7639,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", + "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.37.0", + "@typescript-eslint/tsconfig-utils": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6092,9 +7668,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6118,16 +7694,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz", + "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6142,14 +7718,14 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", + "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.37.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6160,15 +7736,16 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz", - "integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "debug": "^4.4.0", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", @@ -6183,8 +7760,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.1.4", - "vitest": "3.1.4" + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -6193,14 +7770,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", - "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.4", - "@vitest/utils": "3.1.4", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -6209,13 +7787,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", - "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.4", + "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -6224,7 +7802,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -6235,20 +7813,10 @@ } } }, - "node_modules/@vitest/mocker/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/@vitest/pretty-format": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", - "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -6259,27 +7827,28 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", - "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.4", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", - "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.4", + "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -6288,27 +7857,27 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", - "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", - "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.4", - "loupe": "^3.1.3", + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { @@ -6490,19 +8059,15 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, "node_modules/abort-controller": { "version": "3.0.0", @@ -6529,10 +8094,19 @@ "node": ">= 0.6" } }, + "node_modules/accepts/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -6561,16 +8135,12 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "license": "MIT", - "dependencies": { - "debug": "4" - }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/ajv": { @@ -6707,8 +8277,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", @@ -6794,9 +8363,9 @@ } }, "node_modules/archiver-utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -6837,12 +8406,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/archiver-utils/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, "node_modules/archiver-utils/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -6908,8 +8471,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", @@ -6957,6 +8519,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", + "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -6975,16 +8549,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -7005,9 +8569,9 @@ "optional": true }, "node_modules/bare-fs": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.2.tgz", - "integrity": "sha512-8wSeOia5B7LwD4+h465y73KOdj5QHsbbuoUfPBi+pXgFJIPuG7SsiOdJuijWMyfid49eD+WivpfY7KT8gbAzBA==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz", + "integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -7136,9 +8700,9 @@ } }, "node_modules/bignumber.js": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.2.1.tgz", - "integrity": "sha512-+NzaKgOUvInq9TIUZ1+DRspzf/HApkCwD4btfuasFTdrfnOxqx853TgDpMolp+uv4RpRp7bPcEU2zKr9+fRmyw==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", "license": "MIT", "engines": { "node": "*" @@ -7202,9 +8766,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -7225,9 +8789,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", "dev": true, "funding": [ { @@ -7245,10 +8809,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -7320,9 +8884,9 @@ } }, "node_modules/bullmq": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.53.0.tgz", - "integrity": "sha512-AbzcwR+9GdgrenolOC9kApF+TkUKZpUCMiFbXgRYw9ivWhOfLCqKeajIptM7NdwhY7cpXgv+QpbweUuQZUxkyA==", + "version": "5.56.4", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.56.4.tgz", + "integrity": "sha512-5wSHd0oXs2jS6P+6tay/01Iz0cWRK8iYcscKtpS/GewEq0bJZwbkMZc77GJVOT9SP+UQuXA2y+pQTdCQJel7kQ==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", @@ -7399,38 +8963,15 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/cacache/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/cacache/node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/cacache/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -7468,13 +9009,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/cacache/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -7491,35 +9025,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/cacache/node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/cacache/node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -7537,53 +9042,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cacache/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -7627,15 +9085,15 @@ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001713", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz", - "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==", + "version": "1.0.30001724", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz", + "integrity": "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -7652,6 +9110,28 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.1.2.tgz", + "integrity": "sha512-Z/tzFAcBzoCvJlOSlCnoekh1Gu8YMn0J51+UAuXJAbW1Z6I9l2mZgdD7738MepoeeIcUdDtbMnOg6cC7GJxy/g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, + "node_modules/canvas/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/chai": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", @@ -7702,37 +9182,28 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/chrome-trace-event": { @@ -7761,6 +9232,15 @@ "node": ">=8" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -7857,12 +9337,6 @@ "node": ">= 12" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -8061,16 +9535,16 @@ } }, "node_modules/compression": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", - "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, @@ -8093,13 +9567,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/compression/node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "node_modules/compression/node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/concat-map": { @@ -8138,6 +9612,12 @@ "node": ">= 6" } }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "license": "MIT" + }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", @@ -8220,13 +9700,13 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.41.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", - "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz", + "integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.4" + "browserslist": "^4.25.0" }, "funding": { "type": "opencollective", @@ -8319,13 +9799,16 @@ } }, "node_modules/cron": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/cron/-/cron-3.5.0.tgz", - "integrity": "sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.0.tgz", + "integrity": "sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw==", "license": "MIT", "dependencies": { - "@types/luxon": "~3.4.0", - "luxon": "~3.5.0" + "@types/luxon": "~3.6.0", + "luxon": "~3.6.0" + }, + "engines": { + "node": ">=18.x" } }, "node_modules/cron-parser": { @@ -8341,9 +9824,9 @@ } }, "node_modules/cron/node_modules/luxon": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", - "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", "license": "MIT", "engines": { "node": ">=12" @@ -8368,7 +9851,6 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "license": "MIT", - "peer": true, "bin": { "cssesc": "bin/cssesc" }, @@ -8377,13 +9859,15 @@ } }, "node_modules/cssstyle": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz", - "integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.5.0.tgz", + "integrity": "sha512-/7gw8TGrvH/0g564EnhgFZogTMVe+lifpB7LWU+PEsiq5o83TUXR3fDbzTRXOJhoJwck5IS9ez3Em5LNMMO2aw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@asamuzakjp/css-color": "^3.1.2", + "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" }, "engines": { @@ -8403,6 +9887,8 @@ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" @@ -8411,43 +9897,6 @@ "node": ">=18" } }, - "node_modules/data-urls/node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-urls/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -8455,9 +9904,9 @@ "license": "MIT" }, "node_modules/debounce": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.0.0.tgz", - "integrity": "sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", + "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", "license": "MIT", "engines": { "node": ">=18" @@ -8488,7 +9937,25 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/dedent": { "version": "1.6.0", @@ -8514,6 +9981,16 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -8542,24 +10019,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -8644,8 +10103,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/discontinuous-range": { "version": "1.0.0", @@ -8658,8 +10116,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/docker-compose": { "version": "1.2.0", @@ -8747,9 +10204,9 @@ } }, "node_modules/dockerode/node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "dev": true, "license": "MIT", "dependencies": { @@ -8884,9 +10341,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz", - "integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==", + "version": "1.5.171", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.171.tgz", + "integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==", "dev": true, "license": "ISC" }, @@ -8916,9 +10373,9 @@ } }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, "license": "MIT", "dependencies": { @@ -9023,6 +10480,27 @@ "node": ">= 0.6" } }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -9195,19 +10673,19 @@ } }, "node_modules/eslint": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", - "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.27.0", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -9219,9 +10697,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -9272,14 +10750,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz", - "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", + "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.0" + "synckit": "^0.11.7" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -9365,9 +10843,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9382,9 +10860,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -9408,15 +10886,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9476,11 +10954,14 @@ } }, "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } }, "node_modules/esutils": { "version": "2.0.3", @@ -9542,6 +11023,16 @@ "exiftool-vendored.pl": "13.0.1" } }, + "node_modules/exiftool-vendored.exe": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-13.0.0.tgz", + "integrity": "sha512-4zAMuFGgxZkOoyQIzZMHv1HlvgyJK3AkNqjAgm8A8V0UmOZO7yv3pH49cDV1OduzFJqgs6yQ6eG4OGydhKtxlg==", + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/exiftool-vendored.pl": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-13.0.1.tgz", @@ -9552,6 +11043,16 @@ "!win32" ] }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", @@ -9629,6 +11130,12 @@ "node": ">=6.6.0" } }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -9744,9 +11251,9 @@ } }, "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -9811,18 +11318,18 @@ } }, "node_modules/file-type": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.5.0.tgz", - "integrity": "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", "license": "MIT", "dependencies": { - "@tokenizer/inflate": "^0.2.6", - "strtok3": "^10.2.0", + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sindresorhus/file-type?sponsor=1" @@ -9887,16 +11394,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-yarn-workspace-root": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", - "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "micromatch": "^4.0.2" - } - }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -9922,6 +11419,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "dependencies": { "async": "^0.2.9", @@ -9992,45 +11490,16 @@ "webpack": "^5.11.0" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -10123,29 +11592,16 @@ } }, "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, "license": "ISC", "dependencies": { - "minipass": "^3.0.0" + "minipass": "^7.0.3" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/fs-monkey": { @@ -10162,6 +11618,20 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -10239,28 +11709,6 @@ "node": ">=14" } }, - "node_modules/gaxios/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/gaxios/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/gcp-metadata": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", @@ -10380,6 +11828,13 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, "node_modules/glob": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", @@ -10422,22 +11877,13 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/glob/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" @@ -10447,9 +11893,9 @@ } }, "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -10540,19 +11986,6 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -10614,6 +12047,8 @@ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "whatwg-encoding": "^3.1.1" }, @@ -10664,9 +12099,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "dev": true, "license": "BSD-2-Clause" }, @@ -10686,6 +12121,15 @@ "node": ">= 0.8" } }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -10700,28 +12144,17 @@ "node": ">= 14" } }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/i18n-iso-countries": { @@ -10795,9 +12228,9 @@ } }, "node_modules/import-in-the-middle": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.13.1.tgz", - "integrity": "sha512-k2V9wNm9B+ysuelDTHjI9d5KPc4l8zAZTGqj+pcynvWkypZd857ryzN8jNC7Pg2YZXNMJcHRPpaDyCBbNyVRpA==", + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.14.2.tgz", + "integrity": "sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==", "license": "Apache-2.0", "dependencies": { "acorn": "^8.14.0", @@ -10847,6 +12280,13 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/inquirer": { "version": "8.2.6", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", @@ -11005,22 +12445,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -11083,7 +12507,9 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/is-promise": { "version": "4.0.0", @@ -11135,26 +12561,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -11186,22 +12592,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/istanbul-lib-source-maps": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", @@ -11241,9 +12631,9 @@ } }, "node_modules/jackspeak": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", - "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -11291,24 +12681,24 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } }, "node_modules/jose": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz", - "integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", + "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } }, "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -11336,6 +12726,8 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -11370,89 +12762,6 @@ } } }, - "node_modules/jsdom/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/jsdom/node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/jsdom/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/jsdom/node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -11494,26 +12803,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stable-stringify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz", - "integrity": "sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "isarray": "^2.0.5", - "jsonify": "^0.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -11525,7 +12814,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -11554,16 +12842,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", - "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", - "dev": true, - "license": "Public Domain", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -11574,14 +12852,13 @@ "json-buffer": "3.0.1" } }, - "node_modules/klaw-sync": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "dev": true, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11" + "engines": { + "node": ">=6" } }, "node_modules/kysely": { @@ -11675,9 +12952,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.12.6", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.6.tgz", - "integrity": "sha512-PJiS4ETaUfCOFLpmtKzAbqZQjCCKVu2OhTV4SVNNE7c2nu/dACvtCqj4L0i/KWNnIgRv7yrILvBj5Lonv5Ncxw==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.9.tgz", + "integrity": "sha512-VWwAdNeJgN7jFOD+wN4qx83DTPMVPPAUyx9/TUkBXKLiNkuWWk6anV0439tgdtwaJDrEdqkvdN22iA6J4bUCZg==", "license": "MIT" }, "node_modules/lilconfig": { @@ -11685,7 +12962,6 @@ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "license": "MIT", - "peer": true, "engines": { "node": ">=14" }, @@ -11802,31 +13078,28 @@ } }, "node_modules/long": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", - "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", "dev": true, "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "license": "ISC", - "engines": { - "node": "20 || >=22" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, "node_modules/luxon": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", - "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", "license": "MIT", "engines": { "node": ">=12" @@ -11855,31 +13128,21 @@ } }, "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { - "semver": "^6.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/make-fetch-happen": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", @@ -11903,6 +13166,16 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/marked": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz", @@ -12076,6 +13349,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -12148,19 +13434,6 @@ "encoding": "^0.1.13" } }, - "node_modules/minipass-fetch/node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/minipass-flush": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", @@ -12187,6 +13460,13 @@ "node": ">=8" } }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", @@ -12213,6 +13493,13 @@ "node": ">=8" } }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/minipass-sized": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", @@ -12239,31 +13526,24 @@ "node": ">=8" } }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", "dev": true, "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" + "node": ">= 18" } }, "node_modules/mkdirp": { @@ -12305,9 +13585,9 @@ } }, "node_modules/module-details-from-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", - "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", "license": "MIT" }, "node_modules/moo": { @@ -12333,9 +13613,9 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", - "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.4.tgz", + "integrity": "sha512-uaff7RG9VIC4jacFW9xzL3jc0iM32DNHe4jYVycBcjUePT/Klnfj7pqtWJt9khvDFizmjN2TlYniYmSS2LIaZg==", "license": "MIT", "optionalDependencies": { "msgpackr-extract": "^3.0.2" @@ -12364,44 +13644,23 @@ } }, "node_modules/multer": { - "version": "1.4.5-lts.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", - "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" + "type-is": "^1.6.18", + "xtend": "^4.0.2" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 10.16.0" } }, - "node_modules/multer/node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/multer/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/multer/node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -12432,36 +13691,6 @@ "node": ">= 0.6" } }, - "node_modules/multer/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/multer/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/multer/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/multer/node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -12490,7 +13719,6 @@ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "license": "MIT", - "peer": true, "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -12523,6 +13751,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -12561,9 +13796,9 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -12576,9 +13811,9 @@ "license": "MIT" }, "node_modules/nest-commander": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.17.0.tgz", - "integrity": "sha512-1R9vppZT2j/9njKiG0zYTDLAyQOj14KdGWdNuhluveK8VXoQepXNb0t09dRNWy4KCWrI7wDZ2tQTEwb43JyHOw==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.18.0.tgz", + "integrity": "sha512-NWtodOl2aStnApWp9oajCoJW71lqN0CCjf9ygOWxpXnG3o4nQ8ZO5CgrExfVw2+0CVC877hr0rFR7FSu2rypGg==", "license": "MIT", "dependencies": { "@fig/complete-commander": "^3.0.0", @@ -12630,9 +13865,9 @@ } }, "node_modules/nestjs-kysely": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/nestjs-kysely/-/nestjs-kysely-1.2.0.tgz", - "integrity": "sha512-KseCGb0SXCzIYC+Hx3Z3d+kPAfSZCSK6j9UoqUV/gcBCPad9utC7itmoUw0/w5sV+Jf9pc1DKpgClP1IkflA4w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/nestjs-kysely/-/nestjs-kysely-3.0.0.tgz", + "integrity": "sha512-YA6tHBgXQYPNpMBPII2OvUOiaWjCCoh5pP5dUHirQcMUHxNFzInBL6MDk8y74rk2z/5IvAK9AUlsdPyJtToO6g==", "license": "MIT", "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", @@ -12642,81 +13877,34 @@ } }, "node_modules/nestjs-otel": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/nestjs-otel/-/nestjs-otel-6.2.0.tgz", - "integrity": "sha512-F15GnWNrmHxDRdn0o2/cDx65gR7+s3xou1mEJ5vVONfOOYeneIJi1Mkf6h/Qu6NfO4SHPFPKGMXoovvTX1D8Iw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/nestjs-otel/-/nestjs-otel-7.0.0.tgz", + "integrity": "sha512-BjuzY+fJrlbooIZds15XOHvdv2LrtUiVjIBcV3DMw/VrIXK9szWe+njjMZS6Pqft9hV78M8lJE+Ux2ZowDQ/Hw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/host-metrics": "^0.35.5", + "@opentelemetry/host-metrics": "^0.36.0", "response-time": "^2.3.3" }, + "engines": { + "node": ">= 20" + }, "peerDependencies": { - "@nestjs/common": ">= 10 < 12", - "@nestjs/core": ">= 10 < 12" + "@nestjs/common": ">= 11 < 12", + "@nestjs/core": ">= 11 < 12" } }, - "node_modules/next": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.3.3.tgz", - "integrity": "sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw==", + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "dev": true, "license": "MIT", "dependencies": { - "@next/env": "15.3.3", - "@swc/counter": "0.1.3", - "@swc/helpers": "0.5.15", - "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" + "semver": "^7.3.5" }, "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "15.3.3", - "@next/swc-darwin-x64": "15.3.3", - "@next/swc-linux-arm64-gnu": "15.3.3", - "@next/swc-linux-arm64-musl": "15.3.3", - "@next/swc-linux-x64-gnu": "15.3.3", - "@next/swc-linux-x64-musl": "15.3.3", - "@next/swc-win32-arm64-msvc": "15.3.3", - "@next/swc-win32-x64-msvc": "15.3.3", - "sharp": "^0.34.1" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next/node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" + "node": ">=10" } }, "node_modules/node-abort-controller": { @@ -12726,9 +13914,9 @@ "license": "MIT" }, "node_modules/node-addon-api": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", - "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", "license": "MIT", "engines": { "node": "^18 || ^20 || >= 21" @@ -12764,6 +13952,28 @@ } } }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-gyp": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.2.0.tgz", @@ -12815,26 +14025,6 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/node-gyp/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -12845,69 +14035,6 @@ "node": ">=16" } }, - "node_modules/node-gyp/node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/node-gyp/node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/node-gyp/node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^3.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/node-gyp/node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", @@ -12924,16 +14051,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/node-gyp/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -12942,28 +14059,28 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", - "integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz", + "integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==", "license": "MIT-0", "engines": { "node": ">=6.0.0" } }, "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", "dev": true, "license": "ISC", "dependencies": { - "abbrev": "1" + "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": ">=6" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/normalize-path": { @@ -13000,12 +14117,33 @@ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/nypm": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.0.tgz", + "integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "pathe": "^2.0.3", + "pkg-types": "^2.0.0", + "tinyexec": "^0.3.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } }, "node_modules/oauth4webapi": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.1.tgz", - "integrity": "sha512-txg/jZQwcbaF7PMJgY7aoxc9QuCxHVFMiEkDIJ60DwDz3PbtXPQnrzo+3X4IRYGChIwWLabRBRpf1k9hO9+xrQ==", + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.5.tgz", + "integrity": "sha512-1K88D2GiAydGblHo39NBro5TebGXa+7tYoyIbxvqv3+haDDry7CBE1eSYuNbOSsYCCU6y0gdynVZAkm4YPw4hg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -13041,16 +14179,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/obliterator": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", @@ -13102,31 +14230,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/openid-client": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.5.0.tgz", - "integrity": "sha512-fAfYaTnOYE2kQCqEJGX9KDObW2aw7IQy4jWpU/+3D3WoCFLbix5Hg6qIPQ6Js9r7f8jDUmsnnguRNCSw4wU/IQ==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.6.2.tgz", + "integrity": "sha512-Xya5TNMnnZuTM6DbHdB4q0S3ig2NTAELnii/ASie1xDEr8iiB8zZbO871OWBdrw++sd3hW6bqWjgcmSy1RTWHA==", "license": "MIT", "dependencies": { - "jose": "^6.0.10", - "oauth4webapi": "^3.5.1" + "jose": "^6.0.11", + "oauth4webapi": "^3.5.4" }, "funding": { "url": "https://github.com/sponsors/panva" @@ -13296,6 +14407,8 @@ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "entities": "^6.0.0" }, @@ -13304,11 +14417,13 @@ } }, "node_modules/parse5/node_modules/entities": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", - "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=0.12" }, @@ -13338,105 +14453,6 @@ "node": ">= 0.8" } }, - "node_modules/patch-package": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", - "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^4.1.2", - "ci-info": "^3.7.0", - "cross-spawn": "^7.0.3", - "find-yarn-workspace-root": "^2.0.0", - "fs-extra": "^9.0.0", - "json-stable-stringify": "^1.0.2", - "klaw-sync": "^6.0.0", - "minimist": "^1.2.6", - "open": "^7.4.2", - "rimraf": "^2.6.3", - "semver": "^7.5.3", - "slash": "^2.0.0", - "tmp": "^0.0.33", - "yaml": "^2.2.2" - }, - "bin": { - "patch-package": "index.js" - }, - "engines": { - "node": ">=14", - "npm": ">5" - } - }, - "node_modules/patch-package/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/patch-package/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/patch-package/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/patch-package/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -13488,6 +14504,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-source": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/path-source/-/path-source-0.1.3.tgz", @@ -13520,7 +14545,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, "license": "MIT" }, "node_modules/pathval": { @@ -13555,36 +14579,23 @@ "url": "https://ko-fi.com/killymxi" } }, - "node_modules/peek-readable": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", - "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/pg": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", - "integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==", + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", "dependencies": { - "pg-connection-string": "^2.9.0", - "pg-pool": "^3.10.0", - "pg-protocol": "^1.10.0", + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "engines": { - "node": ">= 8.0.0" + "node": ">= 16.0.0" }, "optionalDependencies": { - "pg-cloudflare": "^1.2.5" + "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" @@ -13596,16 +14607,16 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz", - "integrity": "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", "license": "MIT", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.0.tgz", - "integrity": "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", "license": "MIT" }, "node_modules/pg-int8": { @@ -13618,18 +14629,18 @@ } }, "node_modules/pg-pool": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.0.tgz", - "integrity": "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.0.tgz", - "integrity": "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", "license": "MIT" }, "node_modules/pg-types": { @@ -13664,9 +14675,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -13680,7 +14691,6 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13690,11 +14700,21 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 6" } }, + "node_modules/pkg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz", + "integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -13757,7 +14777,6 @@ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "license": "MIT", - "peer": true, "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -13775,7 +14794,6 @@ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "license": "MIT", - "peer": true, "dependencies": { "camelcase-css": "^2.0.1" }, @@ -13805,7 +14823,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" @@ -13841,7 +14858,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "postcss-selector-parser": "^6.1.1" }, @@ -13857,7 +14873,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -13870,15 +14885,13 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/postgres": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.5.tgz", - "integrity": "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==", + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", "license": "Unlicense", - "peer": true, "engines": { "node": ">=12" }, @@ -13926,6 +14939,85 @@ "node": ">=0.10.0" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/prebuild-install/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -13937,9 +15029,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -14029,6 +15121,19 @@ "node": ">=10" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -14079,9 +15184,9 @@ } }, "node_modules/protobufjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", - "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", + "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -14122,9 +15227,9 @@ } }, "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "dev": true, "license": "MIT", "dependencies": { @@ -14232,6 +15337,32 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -14254,9 +15385,9 @@ } }, "node_modules/react-email": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.0.16.tgz", - "integrity": "sha512-auhFU+nQxAkKkP6lQhPyGsa9exwfUEzp2BwZnjHokCwphZlg30tu4t1LgdKRwGPYsi7XNGy6asbVLAUhOVpzzg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.2.3.tgz", + "integrity": "sha512-LUKyk9nNVFuTqAyp4yCEQFQjBe+s8nl3VauMWuOhBZ4VhGnimbrnv01U8yD2YwzaHKtytS0U659x5dc/0+xu+Q==", "license": "MIT", "dependencies": { "@babel/parser": "^7.27.0", @@ -14267,15 +15398,18 @@ "debounce": "^2.0.0", "esbuild": "^0.25.0", "glob": "^11.0.0", + "jiti": "2.4.2", "log-symbols": "^7.0.0", "mime-types": "^3.0.0", - "next": "^15.3.1", "normalize-path": "^3.0.0", + "nypm": "0.6.0", "ora": "^8.0.0", - "socket.io": "^4.8.1" + "prompts": "2.4.2", + "socket.io": "^4.8.1", + "tsconfig-paths": "4.2.0" }, "bin": { - "email": "dist/cli/index.mjs" + "email": "dist/index.js" }, "engines": { "node": ">=18.0.0" @@ -14293,21 +15427,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/react-email/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/react-email/node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -14362,6 +15481,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-email/node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/react-email/node_modules/log-symbols": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", @@ -14444,19 +15572,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-email/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/react-email/node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -14510,7 +15625,6 @@ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "license": "MIT", - "peer": true, "dependencies": { "pify": "^2.3.0" } @@ -14565,9 +15679,9 @@ } }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -14586,27 +15700,16 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/redis-errors": { @@ -14842,13 +15945,13 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", - "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", + "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -14858,26 +15961,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.0", - "@rollup/rollup-android-arm64": "4.40.0", - "@rollup/rollup-darwin-arm64": "4.40.0", - "@rollup/rollup-darwin-x64": "4.40.0", - "@rollup/rollup-freebsd-arm64": "4.40.0", - "@rollup/rollup-freebsd-x64": "4.40.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", - "@rollup/rollup-linux-arm-musleabihf": "4.40.0", - "@rollup/rollup-linux-arm64-gnu": "4.40.0", - "@rollup/rollup-linux-arm64-musl": "4.40.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", - "@rollup/rollup-linux-riscv64-gnu": "4.40.0", - "@rollup/rollup-linux-riscv64-musl": "4.40.0", - "@rollup/rollup-linux-s390x-gnu": "4.40.0", - "@rollup/rollup-linux-x64-gnu": "4.40.0", - "@rollup/rollup-linux-x64-musl": "4.40.0", - "@rollup/rollup-win32-arm64-msvc": "4.40.0", - "@rollup/rollup-win32-ia32-msvc": "4.40.0", - "@rollup/rollup-win32-x64-msvc": "4.40.0", + "@rollup/rollup-android-arm-eabi": "4.44.0", + "@rollup/rollup-android-arm64": "4.44.0", + "@rollup/rollup-darwin-arm64": "4.44.0", + "@rollup/rollup-darwin-x64": "4.44.0", + "@rollup/rollup-freebsd-arm64": "4.44.0", + "@rollup/rollup-freebsd-x64": "4.44.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", + "@rollup/rollup-linux-arm-musleabihf": "4.44.0", + "@rollup/rollup-linux-arm64-gnu": "4.44.0", + "@rollup/rollup-linux-arm64-musl": "4.44.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-musl": "4.44.0", + "@rollup/rollup-linux-s390x-gnu": "4.44.0", + "@rollup/rollup-linux-x64-gnu": "4.44.0", + "@rollup/rollup-linux-x64-musl": "4.44.0", + "@rollup/rollup-win32-arm64-msvc": "4.44.0", + "@rollup/rollup-win32-ia32-msvc": "4.44.0", + "@rollup/rollup-win32-x64-msvc": "4.44.0", "fsevents": "~2.3.2" } }, @@ -14902,7 +16005,9 @@ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/run-async": { "version": "2.4.1", @@ -15000,6 +16105,8 @@ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "xmlchars": "^2.2.0" }, @@ -15110,24 +16217,6 @@ "dev": true, "license": "ISC" }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -15233,12 +16322,6 @@ "node": ">=8" } }, - "node_modules/shimmer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", - "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", - "license": "BSD-2-Clause" - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -15330,6 +16413,53 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -15359,15 +16489,11 @@ "node": ">=18" } }, - "node_modules/slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" }, "node_modules/slice-source": { "version": "0.4.1", @@ -15431,6 +16557,27 @@ } } }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -15522,9 +16669,9 @@ } }, "node_modules/socks": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", - "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", + "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", "dev": true, "license": "MIT", "dependencies": { @@ -15551,16 +16698,6 @@ "node": ">= 14" } }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -15614,9 +16751,9 @@ "license": "BSD-3-Clause" }, "node_modules/sql-formatter": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.6.2.tgz", - "integrity": "sha512-ZjqOfJGuB97UeHzTJoTbadlM0h9ynehtSTHNUbGfXR4HZ4rCIoD2oIW91W+A5oE76k8hl0Uz5GD8Sx3Pt9Xa3w==", + "version": "15.6.6", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.6.6.tgz", + "integrity": "sha512-bZydXEXhaNDQBr8xYHC3a8thwcaMuTBp0CkKGjwGYDsIB26tnlWeWPwJtSQ0TEwiJcz9iJJON5mFPkx7XroHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -15628,9 +16765,9 @@ } }, "node_modules/sql-highlight": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.0.0.tgz", - "integrity": "sha512-+fLpbAbWkQ+d0JEchJT/NrRRXbYRNbG15gFpANx73EwxQB1PRjj+k/OI0GTU0J63g8ikGkJECQp9z8XEJZvPRw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", "funding": [ "https://github.com/scriptcoded/sql-highlight?sponsor=1", { @@ -15710,9 +16847,9 @@ "license": "MIT" }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -15752,9 +16889,9 @@ } }, "node_modules/streamx": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", - "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", "license": "MIT", "dependencies": { "fast-fifo": "^1.3.2", @@ -15885,7 +17022,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -15920,14 +17056,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strtok3": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", - "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, "license": "MIT", "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^7.0.0" + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strtok3": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.1.tgz", + "integrity": "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" }, "engines": { "node": ">=18" @@ -15937,35 +17085,11 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -15984,11 +17108,10 @@ } }, "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -15998,7 +17121,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "license": "ISC", - "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -16019,7 +17141,6 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -16030,19 +17151,11 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/sucrase/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC", - "peer": true - }, "node_modules/sucrase/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -16058,7 +17171,6 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -16071,9 +17183,9 @@ } }, "node_modules/superagent": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz", - "integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.2.tgz", + "integrity": "sha512-vWMq11OwWCC84pQaFPzF/VO3BrjkCeewuvJgt1jfV0499Z1QSAWN4EqfMM5WlFDDX9/oP8JjlDKpblrmEoyu4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -16092,14 +17204,14 @@ } }, "node_modules/supertest": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz", - "integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.3.tgz", + "integrity": "sha512-ORY0gPa6ojmg/C74P/bDoS21WL6FMXq5I8mawkEz30/zkwdu0gOeqstFy316vHG6OKxqQ+IbGneRemHI8WraEw==", "dev": true, "license": "MIT", "dependencies": { "methods": "^1.1.2", - "superagent": "^10.2.1" + "superagent": "^10.2.2" }, "engines": { "node": ">=14.18.0" @@ -16153,17 +17265,18 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/synckit": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", - "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.3", - "tslib": "^2.8.1" + "@pkgr/core": "^0.2.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -16203,7 +17316,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -16273,12 +17385,47 @@ "tailwindcss": ">=3.4.17" } }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tailwindcss/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "license": "ISC", - "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -16286,10 +17433,22 @@ "node": ">=10.13.0" } }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tailwindcss/node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -16305,9 +17464,8 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -16315,10 +17473,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", "dev": true, "license": "MIT", "engines": { @@ -16326,27 +17496,27 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "dev": true, "license": "ISC", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-fs": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz", - "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", "dev": true, "license": "MIT", "dependencies": { @@ -16369,38 +17539,31 @@ "streamx": "^2.15.0" } }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "dev": true, "license": "MIT", "bin": { - "mkdirp": "bin/cmd.js" + "mkdirp": "dist/cjs/src/bin.js" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -16502,9 +17665,9 @@ "license": "MIT" }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16544,9 +17707,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16590,13 +17753,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/test-exclude/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/test-exclude/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -16631,27 +17787,27 @@ } }, "node_modules/testcontainers": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.0.2.tgz", - "integrity": "sha512-rI+CiMYGxd1t/fTW+xs79OHkhBIVLCh7Xstm6duYhKH+LZGyoWtXJsljLvkvWV0J8y8FovcG2Z6/1j1KPqM3rA==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.2.1.tgz", + "integrity": "sha512-KJALGi8ButKDZgzHr0PtJUVNBOSlSFncumZ34MCQTN4VEU9AK4tWTn9gCcAFzG4zBmzzC2aEbHMFUujqkbDvBg==", "dev": true, "license": "MIT", "dependencies": { "@balena/dockerignore": "^1.0.2", - "@types/dockerode": "^3.3.39", + "@types/dockerode": "^3.3.42", "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.4.1", "docker-compose": "^1.2.0", - "dockerode": "^4.0.6", + "dockerode": "^4.0.7", "get-port": "^7.1.0", "proper-lockfile": "^4.1.2", "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", - "tar-fs": "^3.0.9", + "tar-fs": "^3.1.0", "tmp": "^0.2.3", - "undici": "^7.10.0" + "undici": "^7.11.0" } }, "node_modules/testcontainers/node_modules/tmp": { @@ -16685,7 +17841,6 @@ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "license": "MIT", - "peer": true, "dependencies": { "any-promise": "^1.0.0" } @@ -16695,7 +17850,6 @@ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "license": "MIT", - "peer": true, "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -16726,13 +17880,12 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16747,9 +17900,9 @@ } }, "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -16767,9 +17920,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, "license": "MIT", "engines": { @@ -16782,6 +17935,8 @@ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "tldts-core": "^6.1.86" }, @@ -16794,7 +17949,9 @@ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/tmp": { "version": "0.0.33", @@ -16861,6 +18018,8 @@ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "dependencies": { "tldts": "^6.1.32" }, @@ -16869,10 +18028,19 @@ } }, "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } }, "node_modules/tree-kill": { "version": "1.2.2", @@ -16910,13 +18078,12 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/tsconfck": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", - "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", "dev": true, "license": "MIT", "bin": { @@ -16938,7 +18105,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, "license": "MIT", "dependencies": { "json5": "^2.2.2", @@ -16971,6 +18137,19 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -17024,9 +18203,9 @@ "license": "MIT" }, "node_modules/typeorm": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.24.tgz", - "integrity": "sha512-4IrHG7A0tY8l5gEGXfW56VOMfUVWEkWlH/h5wmcyZ+V8oCiLj7iTPp0lEjMEZVrxEkGSdP9ErgTKHKXQApl/oA==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.25.tgz", + "integrity": "sha512-fTKDFzWXKwAaBdEMU4k661seZewbNYET4r1J/z3Jwf+eAvlzMVpTLKAVcAzg75WwQk7GDmtsmkZ5MfkmXCiFWg==", "license": "MIT", "dependencies": { "@sqltools/formatter": "^1.2.5", @@ -17130,9 +18309,9 @@ } }, "node_modules/typeorm/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -17197,12 +18376,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/typeorm/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, "node_modules/typeorm/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -17262,15 +18435,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz", + "integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.37.0", + "@typescript-eslint/parser": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -17305,9 +18479,9 @@ "license": "MIT" }, "node_modules/ua-parser-js": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.3.tgz", - "integrity": "sha512-LZyXZdNttONW8LjzEH3Z8+6TE7RfrEiJqDKyh0R11p/kxvrV2o9DrT2FGZO+KVNs3k+drcIQ6C3En6wLnzJGpw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.4.tgz", + "integrity": "sha512-XiBOnM/UpUq21ZZ91q2AVDOnGROE6UQd37WrO9WBgw4u2eGvUCNOheMmZ3EfEUj7DLHr8tre+Um/436Of/Vwzg==", "funding": [ { "type": "opencollective", @@ -17384,9 +18558,9 @@ } }, "node_modules/undici": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", - "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.11.0.tgz", + "integrity": "sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==", "dev": true, "license": "MIT", "engines": { @@ -17445,9 +18619,9 @@ } }, "node_modules/unplugin": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.4.tgz", - "integrity": "sha512-m4PjxTurwpWfpMomp8AptjD5yj8qEZN5uQjjGM3TAs9MWWD2tXSSNNj6jGR2FoVGod4293ytyV6SwBbertfyJg==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.5.tgz", + "integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==", "dev": true, "license": "MIT", "dependencies": { @@ -17460,15 +18634,15 @@ } }, "node_modules/unplugin-swc": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.3.tgz", - "integrity": "sha512-lfBT7Wtauf/1y89xGt+x8+T7yB7bCMq/qXeXcOcqQddKDULGEg/4O2201Eh6eCBxbEi8J1Tmy2scG5dhiBJONg==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.5.tgz", + "integrity": "sha512-BahYtYvQ/KSgOqHoy5FfQgp/oZNAB7jwERxNeFVeN/PtJhg4fpK/ybj9OwKtqGPseOadS7+TGbq6tH2DmDAYvA==", "dev": true, "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.1.4", + "@rollup/pluginutils": "^5.2.0", "load-tsconfig": "^0.2.5", - "unplugin": "^2.3.4" + "unplugin": "^2.3.5" }, "peerDependencies": { "@swc/core": "^1.2.108" @@ -17563,9 +18737,9 @@ } }, "node_modules/validator": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", - "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -17581,15 +18755,18 @@ } }, "node_modules/vite": { - "version": "6.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", - "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -17653,17 +18830,17 @@ } }, "node_modules/vite-node": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", - "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.4.0", + "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -17696,9 +18873,9 @@ } }, "node_modules/vite/node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -17716,7 +18893,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -17725,32 +18902,34 @@ } }, "node_modules/vitest": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", - "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.4", - "@vitest/mocker": "3.1.4", - "@vitest/pretty-format": "^3.1.4", - "@vitest/runner": "3.1.4", - "@vitest/snapshot": "3.1.4", - "@vitest/spy": "3.1.4", - "@vitest/utils": "3.1.4", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", - "debug": "^4.4.0", + "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", + "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.4", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -17766,8 +18945,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.4", - "@vitest/ui": "3.1.4", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, @@ -17801,6 +18980,8 @@ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "xml-name-validator": "^5.0.0" }, @@ -17809,9 +18990,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "dev": true, "license": "MIT", "dependencies": { @@ -17832,10 +19013,16 @@ } }, "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + } }, "node_modules/webpack": { "version": "5.99.6", @@ -17895,9 +19082,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "license": "MIT", "engines": { @@ -18014,9 +19201,9 @@ } }, "node_modules/webpack/node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18039,6 +19226,8 @@ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "iconv-lite": "0.6.3" }, @@ -18052,18 +19241,26 @@ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" } }, "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/which": { @@ -18205,10 +19402,13 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -18231,6 +19431,8 @@ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -18240,7 +19442,9 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/xtend": { "version": "4.0.2", @@ -18261,22 +19465,25 @@ } }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yargs": { diff --git a/server/package.json b/server/package.json index 2d1478b39f..67945b3dbe 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.135.3", + "version": "1.136.0", "description": "", "author": "", "private": true, @@ -29,11 +29,9 @@ "migrations:run": "node ./dist/bin/migrations.js run", "schema:drop": "node ./dist/bin/migrations.js query 'DROP schema public cascade; CREATE schema public;'", "schema:reset": "npm run schema:drop && npm run migrations:run", - "kysely:codegen": "npx kysely-codegen --include-pattern=\"(public|vectors).*\" --dialect postgres --url postgres://postgres:postgres@localhost/immich --log-level debug --out-file=./src/db.d.ts", "sync:open-api": "node ./dist/bin/sync-open-api.js", "sync:sql": "node ./dist/bin/sync-sql.js", - "email:dev": "email dev -p 3050 --dir src/emails", - "postinstall": "patch-package" + "email:dev": "email dev -p 3050 --dir src/emails" }, "dependencies": { "@nestjs/bullmq": "^11.0.1", @@ -42,26 +40,38 @@ "@nestjs/event-emitter": "^3.0.0", "@nestjs/platform-express": "^11.0.4", "@nestjs/platform-socket.io": "^11.0.4", - "@nestjs/schedule": "^5.0.0", + "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.0.2", "@nestjs/websockets": "^11.0.4", - "@opentelemetry/auto-instrumentations-node": "^0.59.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/auto-instrumentations-node": "^0.62.0", "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/exporter-prometheus": "^0.201.0", - "@opentelemetry/sdk-node": "^0.201.0", - "@react-email/components": "^0.0.41", + "@opentelemetry/exporter-prometheus": "^0.203.0", + "@opentelemetry/instrumentation-http": "^0.203.0", + "@opentelemetry/instrumentation-ioredis": "^0.51.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.49.0", + "@opentelemetry/instrumentation-pg": "^0.55.0", + "@opentelemetry/resources": "^2.0.1", + "@opentelemetry/sdk-metrics": "^2.0.1", + "@opentelemetry/sdk-node": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@react-email/components": "^0.3.0", + "@react-email/render": "^1.1.2", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", "bcrypt": "^6.0.0", + "body-parser": "^2.2.0", "bullmq": "^5.51.0", - "chokidar": "^3.5.3", + "chokidar": "^4.0.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "compression": "^1.8.0", "cookie": "^1.0.2", "cookie-parser": "^1.4.7", - "exiftool-vendored": "^28.3.1", + "cron": "4.3.0", + "exiftool-vendored": "^28.8.0", + "express": "^5.1.0", "fast-glob": "^3.3.2", "fluent-ffmpeg": "^2.1.2", "geo-tz": "^8.0.0", @@ -69,19 +79,22 @@ "i18n-iso-countries": "^7.6.0", "ioredis": "^5.3.2", "js-yaml": "^4.1.0", - "kysely": "^0.28.0", + "kysely": "^0.28.2", "kysely-postgres-js": "^2.0.0", "lodash": "^4.17.21", "luxon": "^3.4.2", "mnemonist": "^0.40.3", + "multer": "^2.0.1", "nest-commander": "^3.16.0", "nestjs-cls": "^5.0.0", - "nestjs-kysely": "^1.1.0", - "nestjs-otel": "^6.0.0", + "nestjs-kysely": "^3.0.0", + "nestjs-otel": "^7.0.0", "nodemailer": "^7.0.0", "openid-client": "^6.3.3", "pg": "^8.11.3", + "pg-connection-string": "^2.9.1", "picomatch": "^4.0.2", + "postgres": "3.4.7", "react": "^19.0.0", "react-dom": "^19.0.0", "react-email": "^4.0.0", @@ -92,7 +105,8 @@ "semver": "^7.6.2", "sharp": "^0.34.2", "sirv": "^3.0.0", - "tailwindcss-preset-email": "^1.3.2", + "socket.io": "^4.8.1", + "tailwindcss-preset-email": "^1.4.0", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", "ua-parser-js": "^2.0.0", @@ -110,15 +124,17 @@ "@types/archiver": "^6.0.0", "@types/async-lock": "^1.4.2", "@types/bcrypt": "^5.0.0", + "@types/body-parser": "^1.19.6", "@types/compression": "^1.7.5", "@types/cookie-parser": "^1.4.8", "@types/express": "^5.0.0", "@types/fluent-ffmpeg": "^2.1.21", "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.14.197", + "@types/luxon": "^3.6.2", "@types/mock-fs": "^4.13.1", - "@types/multer": "^1.4.7", - "@types/node": "^22.15.31", + "@types/multer": "^2.0.0", + "@types/node": "^22.16.4", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", @@ -127,17 +143,17 @@ "@types/semver": "^7.5.8", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", + "@types/validator": "^13.15.2", "@vitest/coverage-v8": "^3.0.0", + "canvas": "^3.1.0", "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^59.0.0", "globals": "^16.0.0", - "jsdom": "^26.1.0", "mock-fs": "^5.2.0", "node-addon-api": "^8.3.1", "node-gyp": "^11.2.0", - "patch-package": "^8.0.0", "pngjs": "^7.0.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^4.0.0", @@ -145,6 +161,7 @@ "source-map-support": "^0.5.21", "sql-formatter": "^15.0.0", "supertest": "^7.1.0", + "tailwindcss": "^3.4.0", "testcontainers": "^11.0.0", "tsconfig-paths": "^4.2.0", "typescript": "^5.3.3", @@ -155,7 +172,7 @@ "vitest": "^3.0.0" }, "volta": { - "node": "22.16.0" + "node": "22.17.1" }, "overrides": { "sharp": "^0.34.2" diff --git a/server/patches/postgres+3.4.5.patch b/server/patches/postgres+3.4.5.patch deleted file mode 100644 index 019ef9df78..0000000000 --- a/server/patches/postgres+3.4.5.patch +++ /dev/null @@ -1,48 +0,0 @@ -diff --git a/node_modules/postgres/cf/src/connection.js b/node_modules/postgres/cf/src/connection.js -index ee8b1e6..acf4566 100644 ---- a/node_modules/postgres/cf/src/connection.js -+++ b/node_modules/postgres/cf/src/connection.js -@@ -387,8 +387,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose - } - - function queryError(query, err) { -+ if (!query || typeof query !== 'object' || !query.reject) throw err -+ - 'query' in err || 'parameters' in err || Object.defineProperties(err, { -- stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug }, -+ stack: { value: err.stack + (query.origin || '').replace(/.*\n/, '\n'), enumerable: options.debug }, - query: { value: query.string, enumerable: options.debug }, - parameters: { value: query.parameters, enumerable: options.debug }, - args: { value: query.args, enumerable: options.debug }, -diff --git a/node_modules/postgres/cjs/src/connection.js b/node_modules/postgres/cjs/src/connection.js -index f7f58d1..b7f2d65 100644 ---- a/node_modules/postgres/cjs/src/connection.js -+++ b/node_modules/postgres/cjs/src/connection.js -@@ -385,8 +385,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose - } - - function queryError(query, err) { -+ if (!query || typeof query !== 'object' || !query.reject) throw err -+ - 'query' in err || 'parameters' in err || Object.defineProperties(err, { -- stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug }, -+ stack: { value: err.stack + (query.origin || '').replace(/.*\n/, '\n'), enumerable: options.debug }, - query: { value: query.string, enumerable: options.debug }, - parameters: { value: query.parameters, enumerable: options.debug }, - args: { value: query.args, enumerable: options.debug }, -diff --git a/node_modules/postgres/src/connection.js b/node_modules/postgres/src/connection.js -index 97cc97e..26f508e 100644 ---- a/node_modules/postgres/src/connection.js -+++ b/node_modules/postgres/src/connection.js -@@ -385,8 +385,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose - } - - function queryError(query, err) { -+ if (!query || typeof query !== 'object' || !query.reject) throw err -+ - 'query' in err || 'parameters' in err || Object.defineProperties(err, { -- stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug }, -+ stack: { value: err.stack + (query.origin || '').replace(/.*\n/, '\n'), enumerable: options.debug }, - query: { value: query.string, enumerable: options.debug }, - parameters: { value: query.parameters, enumerable: options.debug }, - args: { value: query.args, enumerable: options.debug }, diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 153b525fe5..8d261463e7 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -5,7 +5,7 @@ import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; import { ClsModule } from 'nestjs-cls'; import { KyselyModule } from 'nestjs-kysely'; import { OpenTelemetryModule } from 'nestjs-otel'; -import { commands } from 'src/commands'; +import { commandsAndQuestions } from 'src/commands'; import { IWorker } from 'src/constants'; import { controllers } from 'src/controllers'; import { ImmichWorker } from 'src/enum'; @@ -73,11 +73,11 @@ class BaseModule implements OnModuleInit, OnModuleDestroy { ); this.eventRepository.setup({ services }); - await this.eventRepository.emit('app.bootstrap'); + await this.eventRepository.emit('AppBootstrap'); } async onModuleDestroy() { - await this.eventRepository.emit('app.shutdown'); + await this.eventRepository.emit('AppShutdown'); await teardownTelemetry(); } } @@ -85,19 +85,19 @@ class BaseModule implements OnModuleInit, OnModuleDestroy { @Module({ imports: [...imports, ScheduleModule.forRoot()], controllers: [...controllers], - providers: [...common, ...middleware, { provide: IWorker, useValue: ImmichWorker.API }], + providers: [...common, ...middleware, { provide: IWorker, useValue: ImmichWorker.Api }], }) export class ApiModule extends BaseModule {} @Module({ imports: [...imports], - providers: [...common, { provide: IWorker, useValue: ImmichWorker.MICROSERVICES }, SchedulerRegistry], + providers: [...common, { provide: IWorker, useValue: ImmichWorker.Microservices }, SchedulerRegistry], }) export class MicroservicesModule extends BaseModule {} @Module({ imports: [...imports], - providers: [...common, ...commands, SchedulerRegistry], + providers: [...common, ...commandsAndQuestions, SchedulerRegistry], }) export class ImmichAdminModule implements OnModuleDestroy { constructor(private service: CliService) {} diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts index b3329e6331..3bdfb3bbc6 100644 --- a/server/src/bin/migrations.ts +++ b/server/src/bin/migrations.ts @@ -107,25 +107,21 @@ const compare = async () => { const { database } = configRepository.getEnv(); const db = postgres(asPostgresConnectionConfig(database.config)); - const source = schemaFromCode(); + const source = schemaFromCode({ overrides: true, namingStrategy: 'default' }); const target = await schemaFromDatabase(db, {}); - const sourceParams = new Set(source.parameters.map(({ name }) => name)); - target.parameters = target.parameters.filter(({ name }) => sourceParams.has(name)); - - const sourceTables = new Set(source.tables.map(({ name }) => name)); - target.tables = target.tables.filter(({ name }) => sourceTables.has(name)); - console.log(source.warnings.join('\n')); const up = schemaDiff(source, target, { tables: { ignoreExtra: true }, functions: { ignoreExtra: false }, + parameters: { ignoreExtra: true }, }); const down = schemaDiff(target, source, { - tables: { ignoreExtra: false }, + tables: { ignoreExtra: false, ignoreMissing: true }, functions: { ignoreExtra: false }, - extension: { ignoreMissing: true }, + extensions: { ignoreMissing: true }, + parameters: { ignoreMissing: true }, }); return { up, down }; diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index a114830e09..6d3cb42fae 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -15,6 +15,7 @@ import { repositories } from 'src/repositories'; import { AccessRepository } from 'src/repositories/access.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { SyncRepository } from 'src/repositories/sync.repository'; import { AuthService } from 'src/services/auth.service'; import { getKyselyConfig } from 'src/utils/database'; @@ -111,7 +112,7 @@ class SqlGenerator { data.push(...(await this.runTargets(instance, `${Repository.name}`))); // nested repositories - if (Repository.name === AccessRepository.name) { + if (Repository.name === AccessRepository.name || Repository.name === SyncRepository.name) { for (const key of Object.keys(instance)) { const subInstance = (instance as any)[key]; data.push(...(await this.runTargets(subInstance, `${Repository.name}.${key}`))); diff --git a/server/src/commands/index.ts b/server/src/commands/index.ts index ce085f6e34..46a8d13e35 100644 --- a/server/src/commands/index.ts +++ b/server/src/commands/index.ts @@ -1,11 +1,16 @@ import { GrantAdminCommand, PromptEmailQuestion, RevokeAdminCommand } from 'src/commands/grant-admin'; import { ListUsersCommand } from 'src/commands/list-users.command'; +import { + ChangeMediaLocationCommand, + PromptConfirmMoveQuestions, + PromptMediaLocationQuestions, +} from 'src/commands/media-location.command'; import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login'; import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login'; import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command'; import { VersionCommand } from 'src/commands/version.command'; -export const commands = [ +export const commandsAndQuestions = [ ResetAdminPasswordCommand, PromptPasswordQuestions, PromptEmailQuestion, @@ -17,4 +22,7 @@ export const commands = [ VersionCommand, GrantAdminCommand, RevokeAdminCommand, + ChangeMediaLocationCommand, + PromptMediaLocationQuestions, + PromptConfirmMoveQuestions, ]; diff --git a/server/src/commands/media-location.command.ts b/server/src/commands/media-location.command.ts new file mode 100644 index 0000000000..0935fe202d --- /dev/null +++ b/server/src/commands/media-location.command.ts @@ -0,0 +1,106 @@ +import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander'; +import { CliService } from 'src/services/cli.service'; + +@Command({ + name: 'change-media-location', + description: 'Change database file paths to align with a new media location', +}) +export class ChangeMediaLocationCommand extends CommandRunner { + constructor( + private service: CliService, + private inquirer: InquirerService, + ) { + super(); + } + + private async showSamplePaths(hint?: string) { + hint = hint ? ` (${hint})` : ''; + + const paths = await this.service.getSampleFilePaths(); + if (paths.length > 0) { + let message = ` Examples from the database${hint}:\n`; + for (const path of paths) { + message += ` - ${path}\n`; + } + + console.log(`\n${message}`); + } + } + + async run(): Promise { + try { + await this.showSamplePaths(); + + const { oldValue, newValue } = await this.inquirer.ask<{ oldValue: string; newValue: string }>( + 'prompt-media-location', + {}, + ); + + const success = await this.service.migrateFilePaths({ + oldValue, + newValue, + confirm: async ({ sourceFolder, targetFolder }) => { + console.log(` + Previous value: ${oldValue} + Current value: ${newValue} + + Changing from "${sourceFolder}/*" to "${targetFolder}/*" +`); + + const { value: confirmed } = await this.inquirer.ask<{ value: boolean }>('prompt-confirm-move', {}); + return confirmed; + }, + }); + + const successMessage = `Matching database file paths were updated successfully! 🎉 + + You may now set IMMICH_MEDIA_LOCATION=${newValue} and restart! + + (please remember to update applicable volume mounts e.g + services: + immich-server: + ... + volumes: + - \${UPLOAD_LOCATION}:/usr/src/app/upload + ... + )`; + + console.log(`\n ${success ? successMessage : 'No rows were updated'}\n`); + + await this.showSamplePaths('after'); + } catch (error) { + console.error(error); + console.error('Unable to update database file paths.'); + } + } +} + +const currentValue = process.env.IMMICH_MEDIA_LOCATION || ''; + +const makePrompt = (which: string) => { + return `Enter the ${which} value of IMMICH_MEDIA_LOCATION:${currentValue ? ` [${currentValue}]` : ''}`; +}; + +@QuestionSet({ name: 'prompt-media-location' }) +export class PromptMediaLocationQuestions { + @Question({ message: makePrompt('previous'), name: 'oldValue' }) + oldValue(value: string) { + return value || currentValue; + } + + @Question({ message: makePrompt('new'), name: 'newValue' }) + newValue(value: string) { + return value || currentValue; + } +} + +@QuestionSet({ name: 'prompt-confirm-move' }) +export class PromptConfirmMoveQuestions { + @Question({ + message: 'Do you want to proceed? [Y/n]', + name: 'value', + }) + value(value: string): boolean { + return ['yes', 'y'].includes((value || 'y').toLowerCase()); + } +} diff --git a/server/src/config.ts b/server/src/config.ts index ae4bdcd906..33a6f19ba1 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -8,7 +8,7 @@ import { OAuthTokenEndpointAuthMethod, QueueName, ToneMapping, - TranscodeHWAccel, + TranscodeHardwareAcceleration, TranscodePolicy, VideoCodec, VideoContainer, @@ -42,7 +42,7 @@ export interface SystemConfig { twoPass: boolean; preferredHwDevice: string; transcode: TranscodePolicy; - accel: TranscodeHWAccel; + accel: TranscodeHardwareAcceleration; accelDecode: boolean; tonemap: ToneMapping; }; @@ -101,6 +101,7 @@ export interface SystemConfig { timeout: number; storageLabelClaim: string; storageQuotaClaim: string; + roleClaim: string; }; passwordLogin: { enabled: boolean; @@ -120,6 +121,14 @@ export interface SystemConfig { newVersionCheck: { enabled: boolean; }; + nightlyTasks: { + startTime: string; + databaseCleanup: boolean; + missingThumbnails: boolean; + clusterNewFaces: boolean; + generateMemories: boolean; + syncQuotaUsage: boolean; + }; trash: { enabled: boolean; days: number; @@ -181,39 +190,39 @@ export const defaults = Object.freeze({ preset: 'ultrafast', targetVideoCodec: VideoCodec.H264, acceptedVideoCodecs: [VideoCodec.H264], - targetAudioCodec: AudioCodec.AAC, - acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS, AudioCodec.PCMS16LE], - acceptedContainers: [VideoContainer.MOV, VideoContainer.OGG, VideoContainer.WEBM], + targetAudioCodec: AudioCodec.Aac, + acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.LibOpus, AudioCodec.PcmS16le], + acceptedContainers: [VideoContainer.Mov, VideoContainer.Ogg, VideoContainer.Webm], targetResolution: '720', maxBitrate: '0', bframes: -1, refs: 0, gopSize: 0, temporalAQ: false, - cqMode: CQMode.AUTO, + cqMode: CQMode.Auto, twoPass: false, preferredHwDevice: 'auto', - transcode: TranscodePolicy.REQUIRED, - tonemap: ToneMapping.HABLE, - accel: TranscodeHWAccel.DISABLED, + transcode: TranscodePolicy.Required, + tonemap: ToneMapping.Hable, + accel: TranscodeHardwareAcceleration.Disabled, accelDecode: false, }, job: { - [QueueName.BACKGROUND_TASK]: { concurrency: 5 }, - [QueueName.SMART_SEARCH]: { concurrency: 2 }, - [QueueName.METADATA_EXTRACTION]: { concurrency: 5 }, - [QueueName.FACE_DETECTION]: { concurrency: 2 }, - [QueueName.SEARCH]: { concurrency: 5 }, - [QueueName.SIDECAR]: { concurrency: 5 }, - [QueueName.LIBRARY]: { concurrency: 5 }, - [QueueName.MIGRATION]: { concurrency: 5 }, - [QueueName.THUMBNAIL_GENERATION]: { concurrency: 3 }, - [QueueName.VIDEO_CONVERSION]: { concurrency: 1 }, - [QueueName.NOTIFICATION]: { concurrency: 5 }, + [QueueName.BackgroundTask]: { concurrency: 5 }, + [QueueName.SmartSearch]: { concurrency: 2 }, + [QueueName.MetadataExtraction]: { concurrency: 5 }, + [QueueName.FaceDetection]: { concurrency: 2 }, + [QueueName.Search]: { concurrency: 5 }, + [QueueName.Sidecar]: { concurrency: 5 }, + [QueueName.Library]: { concurrency: 5 }, + [QueueName.Migration]: { concurrency: 5 }, + [QueueName.ThumbnailGeneration]: { concurrency: 3 }, + [QueueName.VideoConversion]: { concurrency: 1 }, + [QueueName.Notification]: { concurrency: 5 }, }, logging: { enabled: true, - level: LogLevel.LOG, + level: LogLevel.Log, }, machineLearning: { enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false', @@ -263,7 +272,8 @@ export const defaults = Object.freeze({ profileSigningAlgorithm: 'none', storageLabelClaim: 'preferred_username', storageQuotaClaim: 'immich_quota', - tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST, + roleClaim: 'immich_role', + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.ClientSecretPost, timeout: 30_000, }, passwordLogin: { @@ -276,12 +286,12 @@ export const defaults = Object.freeze({ }, image: { thumbnail: { - format: ImageFormat.WEBP, + format: ImageFormat.Webp, size: 250, quality: 80, }, preview: { - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, size: 1440, quality: 80, }, @@ -289,13 +299,21 @@ export const defaults = Object.freeze({ extractEmbedded: false, fullsize: { enabled: false, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, }, }, newVersionCheck: { enabled: true, }, + nightlyTasks: { + startTime: '00:00', + databaseCleanup: true, + generateMemories: true, + syncQuotaUsage: true, + missingThumbnails: true, + clusterNewFaces: true, + }, trash: { enabled: true, days: 30, diff --git a/server/src/constants.ts b/server/src/constants.ts index 2e25797938..2d803c2e95 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -25,14 +25,14 @@ export const EXTENSION_NAMES: Record = { } as const; export const VECTOR_EXTENSIONS = [ - DatabaseExtension.VECTORCHORD, - DatabaseExtension.VECTORS, - DatabaseExtension.VECTOR, + DatabaseExtension.VectorChord, + DatabaseExtension.Vectors, + DatabaseExtension.Vector, ] as const; export const VECTOR_INDEX_TABLES = { - [VectorIndex.CLIP]: 'smart_search', - [VectorIndex.FACE]: 'face_search', + [VectorIndex.Clip]: 'smart_search', + [VectorIndex.Face]: 'face_search', } as const; export const VECTORCHORD_LIST_SLACK_FACTOR = 1.2; @@ -47,7 +47,7 @@ export const serverVersion = new SemVer(version); export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 }); export const ONE_HOUR = Duration.fromObject({ hours: 1 }); -export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload'; +export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || '/usr/src/app/upload'; export const MACHINE_LEARNING_PING_TIMEOUT = Number(process.env.MACHINE_LEARNING_PING_TIMEOUT || 2000); export const MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME = Number( diff --git a/server/src/controllers/activity.controller.ts b/server/src/controllers/activity.controller.ts index b91f2902d5..d2d34da102 100644 --- a/server/src/controllers/activity.controller.ts +++ b/server/src/controllers/activity.controller.ts @@ -20,13 +20,13 @@ export class ActivityController { constructor(private service: ActivityService) {} @Get() - @Authenticated({ permission: Permission.ACTIVITY_READ }) + @Authenticated({ permission: Permission.ActivityRead }) getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise { return this.service.getAll(auth, dto); } @Post() - @Authenticated({ permission: Permission.ACTIVITY_CREATE }) + @Authenticated({ permission: Permission.ActivityCreate }) async createActivity( @Auth() auth: AuthDto, @Body() dto: ActivityCreateDto, @@ -40,14 +40,14 @@ export class ActivityController { } @Get('statistics') - @Authenticated({ permission: Permission.ACTIVITY_STATISTICS }) + @Authenticated({ permission: Permission.ActivityStatistics }) getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise { return this.service.getStatistics(auth, dto); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.ACTIVITY_DELETE }) + @Authenticated({ permission: Permission.ActivityDelete }) deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 49ec5a82ea..36c5e0b13b 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -23,24 +23,24 @@ export class AlbumController { constructor(private service: AlbumService) {} @Get() - @Authenticated({ permission: Permission.ALBUM_READ }) + @Authenticated({ permission: Permission.AlbumRead }) getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise { return this.service.getAll(auth, query); } @Post() - @Authenticated({ permission: Permission.ALBUM_CREATE }) + @Authenticated({ permission: Permission.AlbumCreate }) createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise { return this.service.create(auth, dto); } @Get('statistics') - @Authenticated({ permission: Permission.ALBUM_STATISTICS }) + @Authenticated({ permission: Permission.AlbumStatistics }) getAlbumStatistics(@Auth() auth: AuthDto): Promise { return this.service.getStatistics(auth); } - @Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true }) + @Authenticated({ permission: Permission.AlbumRead, sharedLink: true }) @Get(':id') getAlbumInfo( @Auth() auth: AuthDto, @@ -51,7 +51,7 @@ export class AlbumController { } @Patch(':id') - @Authenticated({ permission: Permission.ALBUM_UPDATE }) + @Authenticated({ permission: Permission.AlbumUpdate }) updateAlbumInfo( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -61,13 +61,13 @@ export class AlbumController { } @Delete(':id') - @Authenticated({ permission: Permission.ALBUM_DELETE }) + @Authenticated({ permission: Permission.AlbumDelete }) deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { return this.service.delete(auth, id); } @Put(':id/assets') - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AlbumAssetCreate, sharedLink: true }) addAssetsToAlbum( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -77,7 +77,7 @@ export class AlbumController { } @Delete(':id/assets') - @Authenticated() + @Authenticated({ permission: Permission.AlbumAssetDelete }) removeAssetFromAlbum( @Auth() auth: AuthDto, @Body() dto: BulkIdsDto, @@ -87,7 +87,7 @@ export class AlbumController { } @Put(':id/users') - @Authenticated() + @Authenticated({ permission: Permission.AlbumUserCreate }) addUsersToAlbum( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -97,7 +97,7 @@ export class AlbumController { } @Put(':id/user/:userId') - @Authenticated() + @Authenticated({ permission: Permission.AlbumUserUpdate }) updateAlbumUser( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -108,7 +108,7 @@ export class AlbumController { } @Delete(':id/user/:userId') - @Authenticated() + @Authenticated({ permission: Permission.AlbumUserDelete }) removeUserFromAlbum( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/api-key.controller.spec.ts b/server/src/controllers/api-key.controller.spec.ts index 434fa2b7aa..993ad012cc 100644 --- a/server/src/controllers/api-key.controller.spec.ts +++ b/server/src/controllers/api-key.controller.spec.ts @@ -55,10 +55,17 @@ describe(APIKeyController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/api-keys/123`) - .send({ name: 'new name', permissions: [Permission.ALL] }); + .send({ name: 'new name', permissions: [Permission.All] }); expect(status).toBe(400); expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); }); + + it('should allow updating just the name', async () => { + const { status } = await request(ctx.getHttpServer()) + .put(`/api-keys/${factory.uuid()}`) + .send({ name: 'new name' }); + expect(status).toBe(200); + }); }); describe('DELETE /api-keys/:id', () => { diff --git a/server/src/controllers/api-key.controller.ts b/server/src/controllers/api-key.controller.ts index 08efd753cf..6347a1274a 100644 --- a/server/src/controllers/api-key.controller.ts +++ b/server/src/controllers/api-key.controller.ts @@ -13,25 +13,25 @@ export class APIKeyController { constructor(private service: ApiKeyService) {} @Post() - @Authenticated({ permission: Permission.API_KEY_CREATE }) + @Authenticated({ permission: Permission.ApiKeyCreate }) createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise { return this.service.create(auth, dto); } @Get() - @Authenticated({ permission: Permission.API_KEY_READ }) + @Authenticated({ permission: Permission.ApiKeyRead }) getApiKeys(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Get(':id') - @Authenticated({ permission: Permission.API_KEY_READ }) + @Authenticated({ permission: Permission.ApiKeyRead }) getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.API_KEY_UPDATE }) + @Authenticated({ permission: Permission.ApiKeyUpdate }) updateApiKey( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -42,7 +42,7 @@ export class APIKeyController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.API_KEY_DELETE }) + @Authenticated({ permission: Permission.ApiKeyDelete }) deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index b2c9397580..8e83b77fb0 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -34,7 +34,7 @@ import { UploadFieldName, } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { ImmichHeader, RouteKey } from 'src/enum'; +import { ImmichHeader, Permission, RouteKey } from 'src/enum'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { FileUploadInterceptor, getFiles } from 'src/middleware/file-upload.interceptor'; @@ -45,7 +45,7 @@ import { ImmichFileResponse, sendFile } from 'src/utils/file'; import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation'; @ApiTags('Assets') -@Controller(RouteKey.ASSET) +@Controller(RouteKey.Asset) export class AssetMediaController { constructor( private logger: LoggingRepository, @@ -56,12 +56,12 @@ export class AssetMediaController { @UseInterceptors(AssetUploadInterceptor, FileUploadInterceptor) @ApiConsumes('multipart/form-data') @ApiHeader({ - name: ImmichHeader.CHECKSUM, + name: ImmichHeader.Checksum, description: 'sha1 checksum that can be used for duplicate detection before the file is uploaded', required: false, }) @ApiBody({ description: 'Asset Upload Information', type: AssetMediaCreateDto }) - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetUpload, sharedLink: true }) async uploadAsset( @Auth() auth: AuthDto, @UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles, @@ -80,7 +80,7 @@ export class AssetMediaController { @Get(':id/original') @FileResponse() - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetDownload, sharedLink: true }) async downloadAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -101,7 +101,7 @@ export class AssetMediaController { summary: 'replaceAsset', description: 'Replace the asset with new file, without changing its id', }) - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetReplace, sharedLink: true }) async replaceAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -120,7 +120,7 @@ export class AssetMediaController { @Get(':id/thumbnail') @FileResponse() - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetView, sharedLink: true }) async viewAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -157,7 +157,7 @@ export class AssetMediaController { @Get(':id/video/playback') @FileResponse() - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetView, sharedLink: true }) async playAssetVideo( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index 925b64c8a8..d23785a5ff 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -13,18 +13,18 @@ import { UpdateAssetDto, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { RouteKey } from 'src/enum'; +import { Permission, RouteKey } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { AssetService } from 'src/services/asset.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Assets') -@Controller(RouteKey.ASSET) +@Controller(RouteKey.Asset) export class AssetController { constructor(private service: AssetService) {} @Get('random') - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) @EndpointLifecycle({ deprecatedAt: 'v1.116.0' }) getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise { return this.service.getRandom(auth, dto.count ?? 1); @@ -44,7 +44,7 @@ export class AssetController { } @Get('statistics') - @Authenticated() + @Authenticated({ permission: Permission.AssetStatistics }) getAssetStatistics(@Auth() auth: AuthDto, @Query() dto: AssetStatsDto): Promise { return this.service.getStatistics(auth, dto); } @@ -58,26 +58,26 @@ export class AssetController { @Put() @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.AssetUpdate }) updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise { return this.service.updateAll(auth, dto); } @Delete() @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.AssetDelete }) deleteAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkDeleteDto): Promise { return this.service.deleteAll(auth, dto); } @Get(':id') - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetRead, sharedLink: true }) getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id) as Promise; } @Put(':id') - @Authenticated() + @Authenticated({ permission: Permission.AssetUpdate }) updateAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/auth.controller.spec.ts b/server/src/controllers/auth.controller.spec.ts index 4129b24124..031ef460c2 100644 --- a/server/src/controllers/auth.controller.spec.ts +++ b/server/src/controllers/auth.controller.spec.ts @@ -2,6 +2,7 @@ import { AuthController } from 'src/controllers/auth.controller'; import { LoginResponseDto } from 'src/dtos/auth.dto'; import { AuthService } from 'src/services/auth.service'; import request from 'supertest'; +import { mediumFactory } from 'test/medium.factory'; import { errorDto } from 'test/medium/responses'; import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; @@ -132,6 +133,50 @@ describe(AuthController.name, () => { expect(status).toEqual(201); expect(service.login).toHaveBeenCalledWith(expect.objectContaining({ email: 'admin@local' }), expect.anything()); }); + + it('should auth cookies on a secure connection', async () => { + const loginResponse = mediumFactory.loginResponse(); + service.login.mockResolvedValue(loginResponse); + const { status, body, headers } = await request(ctx.getHttpServer()) + .post('/auth/login') + .send({ name: 'admin', email: 'admin@local', password: 'password' }); + + expect(status).toEqual(201); + expect(body).toEqual(loginResponse); + + const cookies = headers['set-cookie']; + expect(cookies).toHaveLength(3); + expect(cookies[0].split(';').map((item) => item.trim())).toEqual([ + `immich_access_token=${loginResponse.accessToken}`, + 'Max-Age=34560000', + 'Path=/', + expect.stringContaining('Expires='), + 'HttpOnly', + 'SameSite=Lax', + ]); + expect(cookies[1].split(';').map((item) => item.trim())).toEqual([ + 'immich_auth_type=password', + 'Max-Age=34560000', + 'Path=/', + expect.stringContaining('Expires='), + 'HttpOnly', + 'SameSite=Lax', + ]); + expect(cookies[2].split(';').map((item) => item.trim())).toEqual([ + 'immich_is_authenticated=true', + 'Max-Age=34560000', + 'Path=/', + expect.stringContaining('Expires='), + 'SameSite=Lax', + ]); + }); + }); + + describe('POST /auth/logout', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/auth/logout'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); }); describe('POST /auth/change-password', () => { diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 78c611d761..30b0d662f2 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -16,7 +16,7 @@ import { ValidateAccessTokenResponseDto, } from 'src/dtos/auth.dto'; import { UserAdminResponseDto } from 'src/dtos/user.dto'; -import { AuthType, ImmichCookie } from 'src/enum'; +import { AuthType, ImmichCookie, Permission } from 'src/enum'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { AuthService, LoginDetails } from 'src/services/auth.service'; import { respondWithCookie, respondWithoutCookie } from 'src/utils/response'; @@ -36,9 +36,9 @@ export class AuthController { return respondWithCookie(res, body, { isSecure: loginDetails.isSecure, values: [ - { key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken }, - { key: ImmichCookie.AUTH_TYPE, value: AuthType.PASSWORD }, - { key: ImmichCookie.IS_AUTHENTICATED, value: 'true' }, + { key: ImmichCookie.AccessToken, value: body.accessToken }, + { key: ImmichCookie.AuthType, value: AuthType.Password }, + { key: ImmichCookie.IsAuthenticated, value: 'true' }, ], }); } @@ -57,7 +57,7 @@ export class AuthController { @Post('change-password') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.AuthChangePassword }) changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise { return this.service.changePassword(auth, dto); } @@ -70,13 +70,13 @@ export class AuthController { @Res({ passthrough: true }) res: Response, @Auth() auth: AuthDto, ): Promise { - const authType = (request.cookies || {})[ImmichCookie.AUTH_TYPE]; + const authType = (request.cookies || {})[ImmichCookie.AuthType]; const body = await this.service.logout(auth, authType); return respondWithoutCookie(res, body, [ - ImmichCookie.ACCESS_TOKEN, - ImmichCookie.AUTH_TYPE, - ImmichCookie.IS_AUTHENTICATED, + ImmichCookie.AccessToken, + ImmichCookie.AuthType, + ImmichCookie.IsAuthenticated, ]); } @@ -87,19 +87,19 @@ export class AuthController { } @Post('pin-code') - @Authenticated() + @Authenticated({ permission: Permission.PinCodeCreate }) setupPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeSetupDto): Promise { return this.service.setupPinCode(auth, dto); } @Put('pin-code') - @Authenticated() + @Authenticated({ permission: Permission.PinCodeUpdate }) async changePinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise { return this.service.changePinCode(auth, dto); } @Delete('pin-code') - @Authenticated() + @Authenticated({ permission: Permission.PinCodeDelete }) async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeResetDto): Promise { return this.service.resetPinCode(auth, dto); } diff --git a/server/src/controllers/download.controller.ts b/server/src/controllers/download.controller.ts index 880e636dd1..4f5b18e585 100644 --- a/server/src/controllers/download.controller.ts +++ b/server/src/controllers/download.controller.ts @@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { DownloadService } from 'src/services/download.service'; import { asStreamableFile } from 'src/utils/file'; @@ -13,7 +14,7 @@ export class DownloadController { constructor(private service: DownloadService) {} @Post('info') - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetDownload, sharedLink: true }) getDownloadInfo(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise { return this.service.getDownloadInfo(auth, dto); } @@ -21,7 +22,7 @@ export class DownloadController { @Post('archive') @HttpCode(HttpStatus.OK) @FileResponse() - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetDownload, sharedLink: true }) downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise { return this.service.downloadArchive(auth, dto).then(asStreamableFile); } diff --git a/server/src/controllers/duplicate.controller.ts b/server/src/controllers/duplicate.controller.ts index f6b09e6e7a..da6fe4042d 100644 --- a/server/src/controllers/duplicate.controller.ts +++ b/server/src/controllers/duplicate.controller.ts @@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DuplicateResponseDto } from 'src/dtos/duplicate.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { DuplicateService } from 'src/services/duplicate.service'; import { UUIDParamDto } from 'src/validation'; @@ -13,19 +14,19 @@ export class DuplicateController { constructor(private service: DuplicateService) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.DuplicateRead }) getAssetDuplicates(@Auth() auth: AuthDto): Promise { return this.service.getDuplicates(auth); } @Delete() - @Authenticated() + @Authenticated({ permission: Permission.DuplicateDelete }) deleteDuplicates(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { return this.service.deleteAll(auth, dto); } @Delete(':id') - @Authenticated() + @Authenticated({ permission: Permission.DuplicateDelete }) deleteDuplicate(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/face.controller.ts b/server/src/controllers/face.controller.ts index d94cd532f7..20b6db6039 100644 --- a/server/src/controllers/face.controller.ts +++ b/server/src/controllers/face.controller.ts @@ -19,19 +19,19 @@ export class FaceController { constructor(private service: PersonService) {} @Post() - @Authenticated({ permission: Permission.FACE_CREATE }) + @Authenticated({ permission: Permission.FaceCreate }) createFace(@Auth() auth: AuthDto, @Body() dto: AssetFaceCreateDto) { return this.service.createFace(auth, dto); } @Get() - @Authenticated({ permission: Permission.FACE_READ }) + @Authenticated({ permission: Permission.FaceRead }) getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise { return this.service.getFacesById(auth, dto); } @Put(':id') - @Authenticated({ permission: Permission.FACE_UPDATE }) + @Authenticated({ permission: Permission.FaceUpdate }) reassignFacesById( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -41,7 +41,7 @@ export class FaceController { } @Delete(':id') - @Authenticated({ permission: Permission.FACE_DELETE }) + @Authenticated({ permission: Permission.FaceDelete }) deleteFace(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AssetFaceDeleteDto) { return this.service.deleteFace(auth, id, dto); } diff --git a/server/src/controllers/job.controller.ts b/server/src/controllers/job.controller.ts index 7da19e207f..e6b40e6810 100644 --- a/server/src/controllers/job.controller.ts +++ b/server/src/controllers/job.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto'; +import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { JobService } from 'src/services/job.service'; @@ -10,19 +11,19 @@ export class JobController { constructor(private service: JobService) {} @Get() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.JobRead, admin: true }) getAllJobsStatus(): Promise { return this.service.getAllJobsStatus(); } @Post() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.JobCreate, admin: true }) createJob(@Body() dto: JobCreateDto): Promise { return this.service.create(dto); } @Put(':id') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.JobCreate, admin: true }) sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise { return this.service.handleCommand(id, dto); } diff --git a/server/src/controllers/library.controller.ts b/server/src/controllers/library.controller.ts index b8959ca288..e090586f57 100644 --- a/server/src/controllers/library.controller.ts +++ b/server/src/controllers/library.controller.ts @@ -19,32 +19,32 @@ export class LibraryController { constructor(private service: LibraryService) {} @Get() - @Authenticated({ permission: Permission.LIBRARY_READ, admin: true }) + @Authenticated({ permission: Permission.LibraryRead, admin: true }) getAllLibraries(): Promise { return this.service.getAll(); } @Post() - @Authenticated({ permission: Permission.LIBRARY_CREATE, admin: true }) + @Authenticated({ permission: Permission.LibraryCreate, admin: true }) createLibrary(@Body() dto: CreateLibraryDto): Promise { return this.service.create(dto); } @Get(':id') - @Authenticated({ permission: Permission.LIBRARY_READ, admin: true }) + @Authenticated({ permission: Permission.LibraryRead, admin: true }) getLibrary(@Param() { id }: UUIDParamDto): Promise { return this.service.get(id); } @Put(':id') - @Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true }) + @Authenticated({ permission: Permission.LibraryUpdate, admin: true }) updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise { return this.service.update(id, dto); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true }) + @Authenticated({ permission: Permission.LibraryDelete, admin: true }) deleteLibrary(@Param() { id }: UUIDParamDto): Promise { return this.service.delete(id); } @@ -58,14 +58,14 @@ export class LibraryController { } @Get(':id/statistics') - @Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true }) + @Authenticated({ permission: Permission.LibraryStatistics, admin: true }) getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise { return this.service.getStatistics(id); } @Post(':id/scan') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true }) + @Authenticated({ permission: Permission.LibraryUpdate, admin: true }) scanLibrary(@Param() { id }: UUIDParamDto) { return this.service.queueScan(id); } diff --git a/server/src/controllers/memory.controller.spec.ts b/server/src/controllers/memory.controller.spec.ts new file mode 100644 index 0000000000..ac96e54a5b --- /dev/null +++ b/server/src/controllers/memory.controller.spec.ts @@ -0,0 +1,132 @@ +import { MemoryController } from 'src/controllers/memory.controller'; +import { MemoryService } from 'src/services/memory.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(MemoryController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(MemoryService); + + beforeAll(async () => { + ctx = await controllerSetup(MemoryController, [{ provide: MemoryService, useValue: service }]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /memories', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/memories'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('POST /memories', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/memories'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should validate data when type is on this day', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/memories') + .send({ + type: 'on_this_day', + data: {}, + memoryAt: new Date(2021).toISOString(), + }); + + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']), + ); + }); + }); + + describe('GET /memories/statistics', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/memories/statistics'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /memories/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/memories/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/memories/invalid`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + }); + + describe('PUT /memories/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + }); + + describe('DELETE /memories/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/memories/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /memories/:id/assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}/assets`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid/assets`).send({ ids: [] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should require a valid asset id', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/memories/${factory.uuid()}/assets`) + .send({ ids: ['invalid'] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + }); + }); + + describe('DELETE /memories/:id/assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/memories/${factory.uuid()}/assets`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).delete(`/memories/invalid/assets`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should require a valid asset id', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .delete(`/memories/${factory.uuid()}/assets`) + .send({ ids: ['invalid'] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + }); + }); +}); diff --git a/server/src/controllers/memory.controller.ts b/server/src/controllers/memory.controller.ts index d33c5ec22c..786f2af8a4 100644 --- a/server/src/controllers/memory.controller.ts +++ b/server/src/controllers/memory.controller.ts @@ -20,31 +20,31 @@ export class MemoryController { constructor(private service: MemoryService) {} @Get() - @Authenticated({ permission: Permission.MEMORY_READ }) + @Authenticated({ permission: Permission.MemoryRead }) searchMemories(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise { return this.service.search(auth, dto); } @Post() - @Authenticated({ permission: Permission.MEMORY_CREATE }) + @Authenticated({ permission: Permission.MemoryCreate }) createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise { return this.service.create(auth, dto); } @Get('statistics') - @Authenticated({ permission: Permission.MEMORY_READ }) + @Authenticated({ permission: Permission.MemoryStatistics }) memoriesStatistics(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise { return this.service.statistics(auth, dto); } @Get(':id') - @Authenticated({ permission: Permission.MEMORY_READ }) + @Authenticated({ permission: Permission.MemoryRead }) getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.MEMORY_UPDATE }) + @Authenticated({ permission: Permission.MemoryUpdate }) updateMemory( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -55,13 +55,13 @@ export class MemoryController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.MEMORY_DELETE }) + @Authenticated({ permission: Permission.MemoryDelete }) deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } @Put(':id/assets') - @Authenticated() + @Authenticated({ permission: Permission.MemoryAssetCreate }) addMemoryAssets( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -72,7 +72,7 @@ export class MemoryController { @Delete(':id/assets') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.MemoryAssetDelete }) removeMemoryAssets( @Auth() auth: AuthDto, @Body() dto: BulkIdsDto, diff --git a/server/src/controllers/notification.controller.ts b/server/src/controllers/notification.controller.ts index c64f786850..af4eb198b6 100644 --- a/server/src/controllers/notification.controller.ts +++ b/server/src/controllers/notification.controller.ts @@ -19,31 +19,31 @@ export class NotificationController { constructor(private service: NotificationService) {} @Get() - @Authenticated({ permission: Permission.NOTIFICATION_READ }) + @Authenticated({ permission: Permission.NotificationRead }) getNotifications(@Auth() auth: AuthDto, @Query() dto: NotificationSearchDto): Promise { return this.service.search(auth, dto); } @Put() - @Authenticated({ permission: Permission.NOTIFICATION_UPDATE }) + @Authenticated({ permission: Permission.NotificationUpdate }) updateNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationUpdateAllDto): Promise { return this.service.updateAll(auth, dto); } @Delete() - @Authenticated({ permission: Permission.NOTIFICATION_DELETE }) + @Authenticated({ permission: Permission.NotificationDelete }) deleteNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationDeleteAllDto): Promise { return this.service.deleteAll(auth, dto); } @Get(':id') - @Authenticated({ permission: Permission.NOTIFICATION_READ }) + @Authenticated({ permission: Permission.NotificationRead }) getNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.NOTIFICATION_UPDATE }) + @Authenticated({ permission: Permission.NotificationUpdate }) updateNotification( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -53,7 +53,7 @@ export class NotificationController { } @Delete(':id') - @Authenticated({ permission: Permission.NOTIFICATION_DELETE }) + @Authenticated({ permission: Permission.NotificationDelete }) deleteNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts index 23ddff5ddc..7da75f573a 100644 --- a/server/src/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -41,8 +41,8 @@ export class OAuthController { { isSecure: loginDetails.isSecure, values: [ - { key: ImmichCookie.OAUTH_STATE, value: state }, - { key: ImmichCookie.OAUTH_CODE_VERIFIER, value: codeVerifier }, + { key: ImmichCookie.OAuthState, value: state }, + { key: ImmichCookie.OAuthCodeVerifier, value: codeVerifier }, ], }, ); @@ -56,14 +56,14 @@ export class OAuthController { @GetLoginDetails() loginDetails: LoginDetails, ): Promise { const body = await this.service.callback(dto, request.headers, loginDetails); - res.clearCookie(ImmichCookie.OAUTH_STATE); - res.clearCookie(ImmichCookie.OAUTH_CODE_VERIFIER); + res.clearCookie(ImmichCookie.OAuthState); + res.clearCookie(ImmichCookie.OAuthCodeVerifier); return respondWithCookie(res, body, { isSecure: loginDetails.isSecure, values: [ - { key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken }, - { key: ImmichCookie.AUTH_TYPE, value: AuthType.OAUTH }, - { key: ImmichCookie.IS_AUTHENTICATED, value: 'true' }, + { key: ImmichCookie.AccessToken, value: body.accessToken }, + { key: ImmichCookie.AuthType, value: AuthType.OAuth }, + { key: ImmichCookie.IsAuthenticated, value: 'true' }, ], }); } diff --git a/server/src/controllers/partner.controller.ts b/server/src/controllers/partner.controller.ts index 6830fdd52f..6b6efaa570 100644 --- a/server/src/controllers/partner.controller.ts +++ b/server/src/controllers/partner.controller.ts @@ -13,19 +13,19 @@ export class PartnerController { constructor(private service: PartnerService) {} @Get() - @Authenticated({ permission: Permission.PARTNER_READ }) + @Authenticated({ permission: Permission.PartnerRead }) getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise { return this.service.search(auth, dto); } @Post(':id') - @Authenticated({ permission: Permission.PARTNER_CREATE }) + @Authenticated({ permission: Permission.PartnerCreate }) createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.create(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.PARTNER_UPDATE }) + @Authenticated({ permission: Permission.PartnerUpdate }) updatePartner( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -35,7 +35,7 @@ export class PartnerController { } @Delete(':id') - @Authenticated({ permission: Permission.PARTNER_DELETE }) + @Authenticated({ permission: Permission.PartnerDelete }) removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/person.controller.spec.ts b/server/src/controllers/person.controller.spec.ts index 0366829336..5b63fcc6cd 100644 --- a/server/src/controllers/person.controller.spec.ts +++ b/server/src/controllers/person.controller.spec.ts @@ -60,6 +60,29 @@ describe(PersonController.name, () => { }); }); + describe('DELETE /people', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete('/people'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require uuids in the body', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .delete('/people') + .send({ ids: ['invalid'] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + }); + + it('should respond with 204', async () => { + const { status } = await request(ctx.getHttpServer()) + .delete(`/people`) + .send({ ids: [factory.uuid()] }); + expect(status).toBe(204); + expect(service.deleteAll).toHaveBeenCalled(); + }); + }); + describe('GET /people/:id', () => { it('should be an authenticated route', async () => { await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`); @@ -156,6 +179,25 @@ describe(PersonController.name, () => { }); }); + describe('DELETE /people/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/people/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).delete(`/people/invalid`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + }); + + it('should respond with 204', async () => { + const { status } = await request(ctx.getHttpServer()).delete(`/people/${factory.uuid()}`); + expect(status).toBe(204); + expect(service.delete).toHaveBeenCalled(); + }); + }); + describe('POST /people/:id/merge', () => { it('should be an authenticated route', async () => { await request(ctx.getHttpServer()).post(`/people/${factory.uuid()}/merge`); diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts index 3440042eda..ec66f7a9ca 100644 --- a/server/src/controllers/person.controller.ts +++ b/server/src/controllers/person.controller.ts @@ -1,7 +1,20 @@ -import { Body, Controller, Get, Next, Param, Post, Put, Query, Res } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Next, + Param, + Post, + Put, + Query, + Res, +} from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; -import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceUpdateDto, @@ -32,31 +45,38 @@ export class PersonController { } @Get() - @Authenticated({ permission: Permission.PERSON_READ }) + @Authenticated({ permission: Permission.PersonRead }) getAllPeople(@Auth() auth: AuthDto, @Query() options: PersonSearchDto): Promise { return this.service.getAll(auth, options); } @Post() - @Authenticated({ permission: Permission.PERSON_CREATE }) + @Authenticated({ permission: Permission.PersonCreate }) createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise { return this.service.create(auth, dto); } @Put() - @Authenticated({ permission: Permission.PERSON_UPDATE }) + @Authenticated({ permission: Permission.PersonUpdate }) updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise { return this.service.updateAll(auth, dto); } + @Delete() + @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ permission: Permission.PersonDelete }) + deletePeople(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { + return this.service.deleteAll(auth, dto); + } + @Get(':id') - @Authenticated({ permission: Permission.PERSON_READ }) + @Authenticated({ permission: Permission.PersonRead }) getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.PERSON_UPDATE }) + @Authenticated({ permission: Permission.PersonUpdate }) updatePerson( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -65,15 +85,22 @@ export class PersonController { return this.service.update(auth, id, dto); } + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ permission: Permission.PersonDelete }) + deletePerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.delete(auth, id); + } + @Get(':id/statistics') - @Authenticated({ permission: Permission.PERSON_STATISTICS }) + @Authenticated({ permission: Permission.PersonStatistics }) getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getStatistics(auth, id); } @Get(':id/thumbnail') @FileResponse() - @Authenticated({ permission: Permission.PERSON_READ }) + @Authenticated({ permission: Permission.PersonRead }) async getPersonThumbnail( @Res() res: Response, @Next() next: NextFunction, @@ -84,7 +111,7 @@ export class PersonController { } @Put(':id/reassign') - @Authenticated({ permission: Permission.PERSON_REASSIGN }) + @Authenticated({ permission: Permission.PersonReassign }) reassignFaces( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -94,7 +121,7 @@ export class PersonController { } @Post(':id/merge') - @Authenticated({ permission: Permission.PERSON_MERGE }) + @Authenticated({ permission: Permission.PersonMerge }) mergePerson( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts index 9bda1fcada..fefa916fe7 100644 --- a/server/src/controllers/search.controller.ts +++ b/server/src/controllers/search.controller.ts @@ -16,6 +16,7 @@ import { SmartSearchDto, StatisticsSearchDto, } from 'src/dtos/search.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { SearchService } from 'src/services/search.service'; @@ -26,58 +27,58 @@ export class SearchController { @Post('metadata') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) searchAssets(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise { return this.service.searchMetadata(auth, dto); } @Post('statistics') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.AssetStatistics }) searchAssetStatistics(@Auth() auth: AuthDto, @Body() dto: StatisticsSearchDto): Promise { return this.service.searchStatistics(auth, dto); } @Post('random') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise { return this.service.searchRandom(auth, dto); } @Post('smart') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise { return this.service.searchSmart(auth, dto); } @Get('explore') - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) getExploreData(@Auth() auth: AuthDto): Promise { return this.service.getExploreData(auth); } @Get('person') - @Authenticated() + @Authenticated({ permission: Permission.PersonRead }) searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise { return this.service.searchPerson(auth, dto); } @Get('places') - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) searchPlaces(@Query() dto: SearchPlacesDto): Promise { return this.service.searchPlaces(dto); } @Get('cities') - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) getAssetsByCity(@Auth() auth: AuthDto): Promise { return this.service.getAssetsByCity(auth); } @Get('suggestions') - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise { // TODO fix open api generation to indicate that results can be nullable return this.service.getSearchSuggestions(auth, dto) as Promise; diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index 3544fce2a0..9a1004c280 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -15,6 +15,7 @@ import { ServerVersionResponseDto, } from 'src/dtos/server.dto'; import { VersionCheckStateResponseDto } from 'src/dtos/system-metadata.dto'; +import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { ServerService } from 'src/services/server.service'; import { SystemMetadataService } from 'src/services/system-metadata.service'; @@ -30,19 +31,19 @@ export class ServerController { ) {} @Get('about') - @Authenticated() + @Authenticated({ permission: Permission.ServerAbout }) getAboutInfo(): Promise { return this.service.getAboutInfo(); } @Get('apk-links') - @Authenticated() + @Authenticated({ permission: Permission.ServerApkLinks }) getApkLinks(): ServerApkLinksDto { return this.service.getApkLinks(); } @Get('storage') - @Authenticated() + @Authenticated({ permission: Permission.ServerStorage }) getStorage(): Promise { return this.service.getStorage(); } @@ -78,7 +79,7 @@ export class ServerController { } @Get('statistics') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ServerStatistics, admin: true }) getServerStatistics(): Promise { return this.service.getStatistics(); } @@ -88,25 +89,25 @@ export class ServerController { return this.service.getSupportedMediaTypes(); } + @Get('license') + @Authenticated({ permission: Permission.ServerLicenseRead, admin: true }) + @ApiNotFoundResponse() + getServerLicense(): Promise { + return this.service.getLicense(); + } + @Put('license') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ServerLicenseUpdate, admin: true }) setServerLicense(@Body() license: LicenseKeyDto): Promise { return this.service.setLicense(license); } @Delete('license') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ServerLicenseDelete, admin: true }) deleteServerLicense(): Promise { return this.service.deleteLicense(); } - @Get('license') - @Authenticated({ admin: true }) - @ApiNotFoundResponse() - getServerLicense(): Promise { - return this.service.getLicense(); - } - @Get('version-check') @Authenticated() getVersionCheck(): Promise { diff --git a/server/src/controllers/session.controller.ts b/server/src/controllers/session.controller.ts index 3838d5af80..cbe8158fee 100644 --- a/server/src/controllers/session.controller.ts +++ b/server/src/controllers/session.controller.ts @@ -1,7 +1,7 @@ -import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; -import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto } from 'src/dtos/session.dto'; +import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto, SessionUpdateDto } from 'src/dtos/session.dto'; import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { SessionService } from 'src/services/session.service'; @@ -13,33 +13,43 @@ export class SessionController { constructor(private service: SessionService) {} @Post() - @Authenticated({ permission: Permission.SESSION_CREATE }) + @Authenticated({ permission: Permission.SessionCreate }) createSession(@Auth() auth: AuthDto, @Body() dto: SessionCreateDto): Promise { return this.service.create(auth, dto); } @Get() - @Authenticated({ permission: Permission.SESSION_READ }) + @Authenticated({ permission: Permission.SessionRead }) getSessions(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Delete() - @Authenticated({ permission: Permission.SESSION_DELETE }) + @Authenticated({ permission: Permission.SessionDelete }) @HttpCode(HttpStatus.NO_CONTENT) deleteAllSessions(@Auth() auth: AuthDto): Promise { return this.service.deleteAll(auth); } + @Put(':id') + @Authenticated({ permission: Permission.SessionUpdate }) + updateSession( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: SessionUpdateDto, + ): Promise { + return this.service.update(auth, id, dto); + } + @Delete(':id') - @Authenticated({ permission: Permission.SESSION_DELETE }) + @Authenticated({ permission: Permission.SessionDelete }) @HttpCode(HttpStatus.NO_CONTENT) deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } @Post(':id/lock') - @Authenticated({ permission: Permission.SESSION_LOCK }) + @Authenticated({ permission: Permission.SessionLock }) @HttpCode(HttpStatus.NO_CONTENT) lockSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.lock(auth, id); diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index ca978f03da..273d625ca7 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -24,7 +24,7 @@ export class SharedLinkController { constructor(private service: SharedLinkService) {} @Get() - @Authenticated({ permission: Permission.SHARED_LINK_READ }) + @Authenticated({ permission: Permission.SharedLinkRead }) getAllSharedLinks(@Auth() auth: AuthDto, @Query() dto: SharedLinkSearchDto): Promise { return this.service.getAll(auth, dto); } @@ -38,31 +38,31 @@ export class SharedLinkController { @Res({ passthrough: true }) res: Response, @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const sharedLinkToken = request.cookies?.[ImmichCookie.SHARED_LINK_TOKEN]; + const sharedLinkToken = request.cookies?.[ImmichCookie.SharedLinkToken]; if (sharedLinkToken) { dto.token = sharedLinkToken; } const body = await this.service.getMine(auth, dto); return respondWithCookie(res, body, { isSecure: loginDetails.isSecure, - values: body.token ? [{ key: ImmichCookie.SHARED_LINK_TOKEN, value: body.token }] : [], + values: body.token ? [{ key: ImmichCookie.SharedLinkToken, value: body.token }] : [], }); } @Get(':id') - @Authenticated({ permission: Permission.SHARED_LINK_READ }) + @Authenticated({ permission: Permission.SharedLinkRead }) getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Post() - @Authenticated({ permission: Permission.SHARED_LINK_CREATE }) + @Authenticated({ permission: Permission.SharedLinkCreate }) createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) { return this.service.create(auth, dto); } @Patch(':id') - @Authenticated({ permission: Permission.SHARED_LINK_UPDATE }) + @Authenticated({ permission: Permission.SharedLinkUpdate }) updateSharedLink( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -72,7 +72,7 @@ export class SharedLinkController { } @Delete(':id') - @Authenticated({ permission: Permission.SHARED_LINK_DELETE }) + @Authenticated({ permission: Permission.SharedLinkDelete }) removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/stack.controller.ts b/server/src/controllers/stack.controller.ts index 188952eba5..5b153a163b 100644 --- a/server/src/controllers/stack.controller.ts +++ b/server/src/controllers/stack.controller.ts @@ -6,7 +6,7 @@ import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto } from import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { StackService } from 'src/services/stack.service'; -import { UUIDParamDto } from 'src/validation'; +import { UUIDAssetIDParamDto, UUIDParamDto } from 'src/validation'; @ApiTags('Stacks') @Controller('stacks') @@ -14,32 +14,32 @@ export class StackController { constructor(private service: StackService) {} @Get() - @Authenticated({ permission: Permission.STACK_READ }) + @Authenticated({ permission: Permission.StackRead }) searchStacks(@Auth() auth: AuthDto, @Query() query: StackSearchDto): Promise { return this.service.search(auth, query); } @Post() - @Authenticated({ permission: Permission.STACK_CREATE }) + @Authenticated({ permission: Permission.StackCreate }) createStack(@Auth() auth: AuthDto, @Body() dto: StackCreateDto): Promise { return this.service.create(auth, dto); } @Delete() - @Authenticated({ permission: Permission.STACK_DELETE }) + @Authenticated({ permission: Permission.StackDelete }) @HttpCode(HttpStatus.NO_CONTENT) deleteStacks(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { return this.service.deleteAll(auth, dto); } @Get(':id') - @Authenticated({ permission: Permission.STACK_READ }) + @Authenticated({ permission: Permission.StackRead }) getStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.STACK_UPDATE }) + @Authenticated({ permission: Permission.StackUpdate }) updateStack( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -50,8 +50,15 @@ export class StackController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.STACK_DELETE }) + @Authenticated({ permission: Permission.StackDelete }) deleteStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } + + @Delete(':id/assets/:assetId') + @Authenticated({ permission: Permission.StackUpdate }) + @HttpCode(HttpStatus.NO_CONTENT) + removeAssetFromStack(@Auth() auth: AuthDto, @Param() dto: UUIDAssetIDParamDto): Promise { + return this.service.removeAsset(auth, dto); + } } diff --git a/server/src/controllers/sync.controller.spec.ts b/server/src/controllers/sync.controller.spec.ts new file mode 100644 index 0000000000..c1f19ddd66 --- /dev/null +++ b/server/src/controllers/sync.controller.spec.ts @@ -0,0 +1,84 @@ +import { SyncController } from 'src/controllers/sync.controller'; +import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; +import { SyncService } from 'src/services/sync.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(SyncController.name, () => { + let ctx: ControllerContext; + const syncService = mockBaseService(SyncService); + const errorService = { handleError: vi.fn() }; + + beforeAll(async () => { + ctx = await controllerSetup(SyncController, [ + { provide: SyncService, useValue: syncService }, + { provide: GlobalExceptionFilter, useValue: errorService }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + syncService.resetAllMocks(); + errorService.handleError.mockReset(); + ctx.reset(); + }); + + describe('POST /sync/stream', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/sync/stream'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require sync request type enums', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/sync/stream') + .send({ types: ['invalid'] }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest([expect.stringContaining('each value in types must be one of the following values')]), + ); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /sync/ack', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/sync/ack'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('POST /sync/ack', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/sync/ack'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should not allow more than 1,000 entries', async () => { + const acks = Array.from({ length: 1001 }, (_, i) => `ack-${i}`); + const { status, body } = await request(ctx.getHttpServer()).post('/sync/ack').send({ acks }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['acks must contain no more than 1000 elements'])); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('DELETE /sync/ack', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete('/sync/ack'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require sync response type enums', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .delete('/sync/ack') + .send({ types: ['invalid'] }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest([expect.stringContaining('each value in types must be one of the following values')]), + ); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/sync.controller.ts b/server/src/controllers/sync.controller.ts index 0945810be7..a7b2b21a54 100644 --- a/server/src/controllers/sync.controller.ts +++ b/server/src/controllers/sync.controller.ts @@ -12,6 +12,7 @@ import { SyncAckSetDto, SyncStreamDto, } from 'src/dtos/sync.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; import { SyncService } from 'src/services/sync.service'; @@ -41,7 +42,7 @@ export class SyncController { @Post('stream') @Header('Content-Type', 'application/jsonlines+json') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.SyncStream }) async getSyncStream(@Auth() auth: AuthDto, @Res() res: Response, @Body() dto: SyncStreamDto) { try { await this.service.stream(auth, res, dto); @@ -52,21 +53,21 @@ export class SyncController { } @Get('ack') - @Authenticated() + @Authenticated({ permission: Permission.SyncCheckpointRead }) getSyncAck(@Auth() auth: AuthDto): Promise { return this.service.getAcks(auth); } @Post('ack') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.SyncCheckpointUpdate }) sendSyncAck(@Auth() auth: AuthDto, @Body() dto: SyncAckSetDto) { return this.service.setAcks(auth, dto); } @Delete('ack') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.SyncCheckpointDelete }) deleteSyncAck(@Auth() auth: AuthDto, @Body() dto: SyncAckDeleteDto) { return this.service.deleteAcks(auth, dto); } diff --git a/server/src/controllers/system-config.controller.spec.ts b/server/src/controllers/system-config.controller.spec.ts new file mode 100644 index 0000000000..48b8c1bcf0 --- /dev/null +++ b/server/src/controllers/system-config.controller.spec.ts @@ -0,0 +1,74 @@ +import _ from 'lodash'; +import { defaults } from 'src/config'; +import { SystemConfigController } from 'src/controllers/system-config.controller'; +import { StorageTemplateService } from 'src/services/storage-template.service'; +import { SystemConfigService } from 'src/services/system-config.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(SystemConfigController.name, () => { + let ctx: ControllerContext; + const systemConfigService = mockBaseService(SystemConfigService); + const templateService = mockBaseService(StorageTemplateService); + + beforeAll(async () => { + ctx = await controllerSetup(SystemConfigController, [ + { provide: SystemConfigService, useValue: systemConfigService }, + { provide: StorageTemplateService, useValue: templateService }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + systemConfigService.resetAllMocks(); + templateService.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /system-config', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/system-config'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /system-config/defaults', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/system-config/defaults'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /system-config', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put('/system-config'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + describe('nightlyTasks', () => { + it('should validate nightly jobs start time', async () => { + const config = _.cloneDeep(defaults); + config.nightlyTasks.startTime = 'invalid'; + const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['nightlyTasks.startTime must be in HH:mm format'])); + }); + + it('should accept a valid time', async () => { + const config = _.cloneDeep(defaults); + config.nightlyTasks.startTime = '05:05'; + const { status } = await request(ctx.getHttpServer()).put('/system-config').send(config); + expect(status).toBe(200); + }); + + it('should validate a boolean field', async () => { + const config = _.cloneDeep(defaults); + (config.nightlyTasks.databaseCleanup as any) = 'invalid'; + const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['nightlyTasks.databaseCleanup must be a boolean value'])); + }); + }); + }); +}); diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts index 58e8bde87b..69117f4d45 100644 --- a/server/src/controllers/system-config.controller.ts +++ b/server/src/controllers/system-config.controller.ts @@ -15,25 +15,25 @@ export class SystemConfigController { ) {} @Get() - @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) + @Authenticated({ permission: Permission.SystemConfigRead, admin: true }) getConfig(): Promise { return this.service.getSystemConfig(); } @Get('defaults') - @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) + @Authenticated({ permission: Permission.SystemConfigRead, admin: true }) getConfigDefaults(): SystemConfigDto { return this.service.getDefaults(); } @Put() - @Authenticated({ permission: Permission.SYSTEM_CONFIG_UPDATE, admin: true }) + @Authenticated({ permission: Permission.SystemConfigUpdate, admin: true }) updateConfig(@Body() dto: SystemConfigDto): Promise { return this.service.updateSystemConfig(dto); } @Get('storage-template-options') - @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) + @Authenticated({ permission: Permission.SystemConfigRead, admin: true }) getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { return this.storageTemplateService.getStorageTemplateOptions(); } diff --git a/server/src/controllers/system-metadata.controller.ts b/server/src/controllers/system-metadata.controller.ts index 71c37d02c4..ad2245a391 100644 --- a/server/src/controllers/system-metadata.controller.ts +++ b/server/src/controllers/system-metadata.controller.ts @@ -15,26 +15,26 @@ export class SystemMetadataController { constructor(private service: SystemMetadataService) {} @Get('admin-onboarding') - @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) + @Authenticated({ permission: Permission.SystemMetadataRead, admin: true }) getAdminOnboarding(): Promise { return this.service.getAdminOnboarding(); } @Post('admin-onboarding') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.SYSTEM_METADATA_UPDATE, admin: true }) + @Authenticated({ permission: Permission.SystemMetadataUpdate, admin: true }) updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise { return this.service.updateAdminOnboarding(dto); } @Get('reverse-geocoding-state') - @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) + @Authenticated({ permission: Permission.SystemMetadataRead, admin: true }) getReverseGeocodingState(): Promise { return this.service.getReverseGeocodingState(); } @Get('version-check-state') - @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) + @Authenticated({ permission: Permission.SystemMetadataRead, admin: true }) getVersionCheckState(): Promise { return this.service.getVersionCheckState(); } diff --git a/server/src/controllers/tag.controller.ts b/server/src/controllers/tag.controller.ts index cf6b8ac695..4906bc0c6e 100644 --- a/server/src/controllers/tag.controller.ts +++ b/server/src/controllers/tag.controller.ts @@ -21,50 +21,50 @@ export class TagController { constructor(private service: TagService) {} @Post() - @Authenticated({ permission: Permission.TAG_CREATE }) + @Authenticated({ permission: Permission.TagCreate }) createTag(@Auth() auth: AuthDto, @Body() dto: TagCreateDto): Promise { return this.service.create(auth, dto); } @Get() - @Authenticated({ permission: Permission.TAG_READ }) + @Authenticated({ permission: Permission.TagRead }) getAllTags(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Put() - @Authenticated({ permission: Permission.TAG_CREATE }) + @Authenticated({ permission: Permission.TagCreate }) upsertTags(@Auth() auth: AuthDto, @Body() dto: TagUpsertDto): Promise { return this.service.upsert(auth, dto); } @Put('assets') - @Authenticated({ permission: Permission.TAG_ASSET }) + @Authenticated({ permission: Permission.TagAsset }) bulkTagAssets(@Auth() auth: AuthDto, @Body() dto: TagBulkAssetsDto): Promise { return this.service.bulkTagAssets(auth, dto); } @Get(':id') - @Authenticated({ permission: Permission.TAG_READ }) + @Authenticated({ permission: Permission.TagRead }) getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.TAG_UPDATE }) + @Authenticated({ permission: Permission.TagUpdate }) updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: TagUpdateDto): Promise { return this.service.update(auth, id, dto); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.TAG_DELETE }) + @Authenticated({ permission: Permission.TagDelete }) deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } @Put(':id/assets') - @Authenticated({ permission: Permission.TAG_ASSET }) + @Authenticated({ permission: Permission.TagAsset }) tagAssets( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -74,7 +74,7 @@ export class TagController { } @Delete(':id/assets') - @Authenticated({ permission: Permission.TAG_ASSET }) + @Authenticated({ permission: Permission.TagAsset }) untagAssets( @Auth() auth: AuthDto, @Body() dto: BulkIdsDto, diff --git a/server/src/controllers/timeline.controller.spec.ts b/server/src/controllers/timeline.controller.spec.ts new file mode 100644 index 0000000000..6d0276c6a3 --- /dev/null +++ b/server/src/controllers/timeline.controller.spec.ts @@ -0,0 +1,41 @@ +import { TimelineController } from 'src/controllers/timeline.controller'; +import { TimelineService } from 'src/services/timeline.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(TimelineController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(TimelineService); + + beforeAll(async () => { + ctx = await controllerSetup(TimelineController, [{ provide: TimelineService, useValue: service }]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /timeline/buckets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/timeline/buckets'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /timeline/bucket', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/timeline/bucket?timeBucket=1900-01-01'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + // TODO enable date string validation while still accepting 5 digit years + it.fails('should fail if time bucket is invalid', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/timeline/bucket').query({ timeBucket: 'foo' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('Invalid time bucket format')); + }); + }); +}); diff --git a/server/src/controllers/timeline.controller.ts b/server/src/controllers/timeline.controller.ts index b4ee042625..8cab840ec8 100644 --- a/server/src/controllers/timeline.controller.ts +++ b/server/src/controllers/timeline.controller.ts @@ -12,13 +12,13 @@ export class TimelineController { constructor(private service: TimelineService) {} @Get('buckets') - @Authenticated({ permission: Permission.ASSET_READ, sharedLink: true }) + @Authenticated({ permission: Permission.AssetRead, sharedLink: true }) getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto) { return this.service.getTimeBuckets(auth, dto); } @Get('bucket') - @Authenticated({ permission: Permission.ASSET_READ, sharedLink: true }) + @Authenticated({ permission: Permission.AssetRead, sharedLink: true }) @ApiOkResponse({ type: TimeBucketAssetResponseDto }) @Header('Content-Type', 'application/json') getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto) { diff --git a/server/src/controllers/trash.controller.ts b/server/src/controllers/trash.controller.ts index dfcdfa6ba2..1bb46e4f98 100644 --- a/server/src/controllers/trash.controller.ts +++ b/server/src/controllers/trash.controller.ts @@ -14,21 +14,21 @@ export class TrashController { @Post('empty') @HttpCode(HttpStatus.OK) - @Authenticated({ permission: Permission.ASSET_DELETE }) + @Authenticated({ permission: Permission.AssetDelete }) emptyTrash(@Auth() auth: AuthDto): Promise { return this.service.empty(auth); } @Post('restore') @HttpCode(HttpStatus.OK) - @Authenticated({ permission: Permission.ASSET_DELETE }) + @Authenticated({ permission: Permission.AssetDelete }) restoreTrash(@Auth() auth: AuthDto): Promise { return this.service.restore(auth); } @Post('restore/assets') @HttpCode(HttpStatus.OK) - @Authenticated({ permission: Permission.ASSET_DELETE }) + @Authenticated({ permission: Permission.AssetDelete }) restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { return this.service.restoreAssets(auth, dto); } diff --git a/server/src/controllers/user-admin.controller.ts b/server/src/controllers/user-admin.controller.ts index 83d7caef08..d50bd174ad 100644 --- a/server/src/controllers/user-admin.controller.ts +++ b/server/src/controllers/user-admin.controller.ts @@ -21,25 +21,25 @@ export class UserAdminController { constructor(private service: UserAdminService) {} @Get() - @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) + @Authenticated({ permission: Permission.AdminUserRead, admin: true }) searchUsersAdmin(@Auth() auth: AuthDto, @Query() dto: UserAdminSearchDto): Promise { return this.service.search(auth, dto); } @Post() - @Authenticated({ permission: Permission.ADMIN_USER_CREATE, admin: true }) + @Authenticated({ permission: Permission.AdminUserCreate, admin: true }) createUserAdmin(@Body() createUserDto: UserAdminCreateDto): Promise { return this.service.create(createUserDto); } @Get(':id') - @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) + @Authenticated({ permission: Permission.AdminUserRead, admin: true }) getUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true }) + @Authenticated({ permission: Permission.AdminUserUpdate, admin: true }) updateUserAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -49,7 +49,7 @@ export class UserAdminController { } @Delete(':id') - @Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true }) + @Authenticated({ permission: Permission.AdminUserDelete, admin: true }) deleteUserAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -59,7 +59,7 @@ export class UserAdminController { } @Get(':id/statistics') - @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) + @Authenticated({ permission: Permission.AdminUserRead, admin: true }) getUserStatisticsAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -69,13 +69,13 @@ export class UserAdminController { } @Get(':id/preferences') - @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) + @Authenticated({ permission: Permission.AdminUserRead, admin: true }) getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getPreferences(auth, id); } @Put(':id/preferences') - @Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true }) + @Authenticated({ permission: Permission.AdminUserUpdate, admin: true }) updateUserPreferencesAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -85,7 +85,7 @@ export class UserAdminController { } @Post(':id/restore') - @Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true }) + @Authenticated({ permission: Permission.AdminUserDelete, admin: true }) @HttpCode(HttpStatus.OK) restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.restore(auth, id); diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index 6c6eae15ff..1b91e1a848 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -21,7 +21,7 @@ import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto'; -import { RouteKey } from 'src/enum'; +import { Permission, RouteKey } from 'src/enum'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -30,7 +30,7 @@ import { sendFile } from 'src/utils/file'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Users') -@Controller(RouteKey.USER) +@Controller(RouteKey.User) export class UserController { constructor( private service: UserService, @@ -38,31 +38,31 @@ export class UserController { ) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.UserRead }) searchUsers(@Auth() auth: AuthDto): Promise { return this.service.search(auth); } @Get('me') - @Authenticated() + @Authenticated({ permission: Permission.UserRead }) getMyUser(@Auth() auth: AuthDto): Promise { return this.service.getMe(auth); } @Put('me') - @Authenticated() + @Authenticated({ permission: Permission.UserUpdate }) updateMyUser(@Auth() auth: AuthDto, @Body() dto: UserUpdateMeDto): Promise { return this.service.updateMe(auth, dto); } @Get('me/preferences') - @Authenticated() + @Authenticated({ permission: Permission.UserPreferenceRead }) getMyPreferences(@Auth() auth: AuthDto): Promise { return this.service.getMyPreferences(auth); } @Put('me/preferences') - @Authenticated() + @Authenticated({ permission: Permission.UserPreferenceUpdate }) updateMyPreferences( @Auth() auth: AuthDto, @Body() dto: UserPreferencesUpdateDto, @@ -71,43 +71,43 @@ export class UserController { } @Get('me/license') - @Authenticated() + @Authenticated({ permission: Permission.UserLicenseRead }) getUserLicense(@Auth() auth: AuthDto): Promise { return this.service.getLicense(auth); } @Put('me/license') - @Authenticated() + @Authenticated({ permission: Permission.UserLicenseUpdate }) async setUserLicense(@Auth() auth: AuthDto, @Body() license: LicenseKeyDto): Promise { return this.service.setLicense(auth, license); } @Delete('me/license') - @Authenticated() + @Authenticated({ permission: Permission.UserLicenseDelete }) async deleteUserLicense(@Auth() auth: AuthDto): Promise { await this.service.deleteLicense(auth); } @Get('me/onboarding') - @Authenticated() + @Authenticated({ permission: Permission.UserOnboardingRead }) getUserOnboarding(@Auth() auth: AuthDto): Promise { return this.service.getOnboarding(auth); } @Put('me/onboarding') - @Authenticated() + @Authenticated({ permission: Permission.UserOnboardingUpdate }) async setUserOnboarding(@Auth() auth: AuthDto, @Body() Onboarding: OnboardingDto): Promise { return this.service.setOnboarding(auth, Onboarding); } @Delete('me/onboarding') - @Authenticated() + @Authenticated({ permission: Permission.UserOnboardingDelete }) async deleteUserOnboarding(@Auth() auth: AuthDto): Promise { await this.service.deleteOnboarding(auth); } @Get(':id') - @Authenticated() + @Authenticated({ permission: Permission.UserRead }) getUser(@Param() { id }: UUIDParamDto): Promise { return this.service.get(id); } @@ -116,7 +116,7 @@ export class UserController { @ApiConsumes('multipart/form-data') @ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto }) @Post('profile-image') - @Authenticated() + @Authenticated({ permission: Permission.UserProfileImageUpdate }) createProfileImage( @Auth() auth: AuthDto, @UploadedFile() fileInfo: Express.Multer.File, @@ -126,14 +126,14 @@ export class UserController { @Delete('profile-image') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.UserProfileImageDelete }) deleteProfileImage(@Auth() auth: AuthDto): Promise { return this.service.deleteProfileImage(auth); } @Get(':id/profile-image') @FileResponse() - @Authenticated() + @Authenticated({ permission: Permission.UserProfileImageRead }) async getProfileImage(@Res() res: Response, @Next() next: NextFunction, @Param() { id }: UUIDParamDto) { await sendFile(res, next, () => this.service.getProfileImage(id), this.logger); } diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 1a8e31e86b..6576b397e3 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -25,8 +25,8 @@ export interface MoveRequest { }; } -export type GeneratedImageType = AssetPathType.PREVIEW | AssetPathType.THUMBNAIL | AssetPathType.FULLSIZE; -export type GeneratedAssetType = GeneratedImageType | AssetPathType.ENCODED_VIDEO; +export type GeneratedImageType = AssetPathType.Preview | AssetPathType.Thumbnail | AssetPathType.FullSize; +export type GeneratedAssetType = GeneratedImageType | AssetPathType.EncodedVideo; export type ThumbnailPathEntity = { id: string; ownerId: string }; @@ -79,7 +79,7 @@ export class StorageCore { } static getLibraryFolder(user: { storageLabel: string | null; id: string }) { - return join(StorageCore.getBaseFolder(StorageFolder.LIBRARY), user.storageLabel || user.id); + return join(StorageCore.getBaseFolder(StorageFolder.Library), user.storageLabel || user.id); } static getBaseFolder(folder: StorageFolder) { @@ -87,23 +87,23 @@ export class StorageCore { } static getPersonThumbnailPath(person: ThumbnailPathEntity) { - return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`); + return StorageCore.getNestedPath(StorageFolder.Thumbnails, person.ownerId, `${person.id}.jpeg`); } static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: 'jpeg' | 'webp') { - return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`); + return StorageCore.getNestedPath(StorageFolder.Thumbnails, asset.ownerId, `${asset.id}-${type}.${format}`); } static getEncodedVideoPath(asset: ThumbnailPathEntity) { - return StorageCore.getNestedPath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${asset.id}.mp4`); + return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${asset.id}.mp4`); } static getAndroidMotionPath(asset: ThumbnailPathEntity, uuid: string) { - return StorageCore.getNestedPath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${uuid}-MP.mp4`); + return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${uuid}-MP.mp4`); } static isAndroidMotionPath(originalPath: string) { - return originalPath.startsWith(StorageCore.getBaseFolder(StorageFolder.ENCODED_VIDEO)); + return originalPath.startsWith(StorageCore.getBaseFolder(StorageFolder.EncodedVideo)); } static isImmichPath(path: string) { @@ -130,7 +130,7 @@ export class StorageCore { async moveAssetVideo(asset: StorageAsset) { return this.moveFile({ entityId: asset.id, - pathType: AssetPathType.ENCODED_VIDEO, + pathType: AssetPathType.EncodedVideo, oldPath: asset.encodedVideoPath, newPath: StorageCore.getEncodedVideoPath(asset), }); @@ -139,7 +139,7 @@ export class StorageCore { async movePersonFile(person: { id: string; ownerId: string; thumbnailPath: string }, pathType: PersonPathType) { const { id: entityId, thumbnailPath } = person; switch (pathType) { - case PersonPathType.FACE: { + case PersonPathType.Face: { await this.moveFile({ entityId, pathType, @@ -188,7 +188,7 @@ export class StorageCore { move = await this.moveRepository.create({ entityId, pathType, oldPath, newPath }); } - if (pathType === AssetPathType.ORIGINAL && !assetInfo) { + if (pathType === AssetPathType.Original && !assetInfo) { this.logger.warn(`Unable to complete move. Missing asset info for ${entityId}`); return; } @@ -274,25 +274,25 @@ export class StorageCore { private savePath(pathType: PathType, id: string, newPath: string) { switch (pathType) { - case AssetPathType.ORIGINAL: { + case AssetPathType.Original: { return this.assetRepository.update({ id, originalPath: newPath }); } - case AssetPathType.FULLSIZE: { - return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FULLSIZE, path: newPath }); + case AssetPathType.FullSize: { + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FullSize, path: newPath }); } - case AssetPathType.PREVIEW: { - return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: newPath }); + case AssetPathType.Preview: { + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Preview, path: newPath }); } - case AssetPathType.THUMBNAIL: { - return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: newPath }); + case AssetPathType.Thumbnail: { + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Thumbnail, path: newPath }); } - case AssetPathType.ENCODED_VIDEO: { + case AssetPathType.EncodedVideo: { return this.assetRepository.update({ id, encodedVideoPath: newPath }); } - case AssetPathType.SIDECAR: { + case AssetPathType.Sidecar: { return this.assetRepository.update({ id, sidecarPath: newPath }); } - case PersonPathType.FACE: { + case PersonPathType.Face: { return this.personRepository.update({ id, thumbnailPath: newPath }); } } diff --git a/server/src/database.ts b/server/src/database.ts index 1cddda1ee6..53c39b7383 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -1,5 +1,4 @@ import { Selectable } from 'kysely'; -import { Albums, Exif as DatabaseExif } from 'src/db'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { AlbumUserRole, @@ -13,7 +12,9 @@ import { UserAvatarColor, UserStatus, } from 'src/enum'; -import { OnThisDayData, UserMetadataItem } from 'src/types'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; +import { UserMetadataItem } from 'src/types'; export type AuthUser = { id: string; @@ -95,7 +96,7 @@ export type Memory = { showAt: Date | null; hideAt: Date | null; type: MemoryType; - data: OnThisDayData; + data: object; ownerId: string; isSaved: boolean; assets: MapAsset[]; @@ -193,13 +194,14 @@ export type SharedLink = { userId: string; }; -export type Album = Selectable & { +export type Album = Selectable & { owner: User; assets: MapAsset[]; }; export type AuthSession = { id: string; + isPendingSyncReset: boolean; hasElevatedPermission: boolean; }; @@ -237,9 +239,10 @@ export type Session = { deviceOS: string; deviceType: string; pinExpiresAt: Date | null; + isPendingSyncReset: boolean; }; -export type Exif = Omit, 'updatedAt' | 'updateId'>; +export type Exif = Omit, 'updatedAt' | 'updateId'>; export type Person = { createdAt: Date; @@ -269,56 +272,51 @@ export type AssetFace = { personId: string | null; sourceType: SourceType; person?: Person | null; + updatedAt: Date; + updateId: string; }; const userColumns = ['id', 'name', 'email', 'avatarColor', 'profileImagePath', 'profileChangedAt'] as const; const userWithPrefixColumns = [ - 'users.id', - 'users.name', - 'users.email', - 'users.avatarColor', - 'users.profileImagePath', - 'users.profileChangedAt', + 'user2.id', + 'user2.name', + 'user2.email', + 'user2.avatarColor', + 'user2.profileImagePath', + 'user2.profileChangedAt', ] as const; export const columns = { asset: [ - 'assets.id', - 'assets.checksum', - 'assets.deviceAssetId', - 'assets.deviceId', - 'assets.fileCreatedAt', - 'assets.fileModifiedAt', - 'assets.isExternal', - 'assets.visibility', - 'assets.libraryId', - 'assets.livePhotoVideoId', - 'assets.localDateTime', - 'assets.originalFileName', - 'assets.originalPath', - 'assets.ownerId', - 'assets.sidecarPath', - 'assets.type', + 'asset.id', + 'asset.checksum', + 'asset.deviceAssetId', + 'asset.deviceId', + 'asset.fileCreatedAt', + 'asset.fileModifiedAt', + 'asset.isExternal', + 'asset.visibility', + 'asset.libraryId', + 'asset.livePhotoVideoId', + 'asset.localDateTime', + 'asset.originalFileName', + 'asset.originalPath', + 'asset.ownerId', + 'asset.sidecarPath', + 'asset.type', ], - assetFiles: ['asset_files.id', 'asset_files.path', 'asset_files.type'], - authUser: [ - 'users.id', - 'users.name', - 'users.email', - 'users.isAdmin', - 'users.quotaUsageInBytes', - 'users.quotaSizeInBytes', - ], - authApiKey: ['api_keys.id', 'api_keys.permissions'], - authSession: ['sessions.id', 'sessions.updatedAt', 'sessions.pinExpiresAt'], + assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'], + authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'], + authApiKey: ['api_key.id', 'api_key.permissions'], + authSession: ['session.id', 'session.isPendingSyncReset', 'session.updatedAt', 'session.pinExpiresAt'], authSharedLink: [ - 'shared_links.id', - 'shared_links.userId', - 'shared_links.expiresAt', - 'shared_links.showExif', - 'shared_links.allowUpload', - 'shared_links.allowDownload', - 'shared_links.password', + 'shared_link.id', + 'shared_link.userId', + 'shared_link.expiresAt', + 'shared_link.showExif', + 'shared_link.allowUpload', + 'shared_link.allowDownload', + 'shared_link.password', ], user: userColumns, userWithPrefix: userWithPrefixColumns, @@ -336,89 +334,86 @@ export const columns = { 'quotaSizeInBytes', 'quotaUsageInBytes', ], - tag: ['tags.id', 'tags.value', 'tags.createdAt', 'tags.updatedAt', 'tags.color', 'tags.parentId'], + tag: ['tag.id', 'tag.value', 'tag.createdAt', 'tag.updatedAt', 'tag.color', 'tag.parentId'], apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'], notification: ['id', 'createdAt', 'level', 'type', 'title', 'description', 'data', 'readAt'], syncAsset: [ - 'id', - 'ownerId', - 'originalFileName', - 'thumbhash', - 'checksum', - 'fileCreatedAt', - 'fileModifiedAt', - 'localDateTime', - 'type', - 'deletedAt', - 'isFavorite', - 'visibility', - 'updateId', - 'duration', - ], - syncAlbumUser: [ - 'albums_shared_users_users.albumsId as albumId', - 'albums_shared_users_users.usersId as userId', - 'albums_shared_users_users.role', - 'albums_shared_users_users.updateId', + 'asset.id', + 'asset.ownerId', + 'asset.originalFileName', + 'asset.thumbhash', + 'asset.checksum', + 'asset.fileCreatedAt', + 'asset.fileModifiedAt', + 'asset.localDateTime', + 'asset.type', + 'asset.deletedAt', + 'asset.isFavorite', + 'asset.visibility', + 'asset.duration', + 'asset.livePhotoVideoId', + 'asset.stackId', ], + syncAlbumUser: ['album_user.albumsId as albumId', 'album_user.usersId as userId', 'album_user.role'], + syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'], + syncUser: ['id', 'name', 'email', 'avatarColor', 'deletedAt', 'updateId'], stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'], syncAssetExif: [ - 'exif.assetId', - 'exif.description', - 'exif.exifImageWidth', - 'exif.exifImageHeight', - 'exif.fileSizeInByte', - 'exif.orientation', - 'exif.dateTimeOriginal', - 'exif.modifyDate', - 'exif.timeZone', - 'exif.latitude', - 'exif.longitude', - 'exif.projectionType', - 'exif.city', - 'exif.state', - 'exif.country', - 'exif.make', - 'exif.model', - 'exif.lensModel', - 'exif.fNumber', - 'exif.focalLength', - 'exif.iso', - 'exif.exposureTime', - 'exif.profileDescription', - 'exif.rating', - 'exif.fps', - 'exif.updateId', + 'asset_exif.assetId', + 'asset_exif.description', + 'asset_exif.exifImageWidth', + 'asset_exif.exifImageHeight', + 'asset_exif.fileSizeInByte', + 'asset_exif.orientation', + 'asset_exif.dateTimeOriginal', + 'asset_exif.modifyDate', + 'asset_exif.timeZone', + 'asset_exif.latitude', + 'asset_exif.longitude', + 'asset_exif.projectionType', + 'asset_exif.city', + 'asset_exif.state', + 'asset_exif.country', + 'asset_exif.make', + 'asset_exif.model', + 'asset_exif.lensModel', + 'asset_exif.fNumber', + 'asset_exif.focalLength', + 'asset_exif.iso', + 'asset_exif.exposureTime', + 'asset_exif.profileDescription', + 'asset_exif.rating', + 'asset_exif.fps', ], exif: [ - 'exif.assetId', - 'exif.autoStackId', - 'exif.bitsPerSample', - 'exif.city', - 'exif.colorspace', - 'exif.country', - 'exif.dateTimeOriginal', - 'exif.description', - 'exif.exifImageHeight', - 'exif.exifImageWidth', - 'exif.exposureTime', - 'exif.fileSizeInByte', - 'exif.fNumber', - 'exif.focalLength', - 'exif.fps', - 'exif.iso', - 'exif.latitude', - 'exif.lensModel', - 'exif.livePhotoCID', - 'exif.longitude', - 'exif.make', - 'exif.model', - 'exif.modifyDate', - 'exif.orientation', - 'exif.profileDescription', - 'exif.projectionType', - 'exif.rating', - 'exif.state', - 'exif.timeZone', + 'asset_exif.assetId', + 'asset_exif.autoStackId', + 'asset_exif.bitsPerSample', + 'asset_exif.city', + 'asset_exif.colorspace', + 'asset_exif.country', + 'asset_exif.dateTimeOriginal', + 'asset_exif.description', + 'asset_exif.exifImageHeight', + 'asset_exif.exifImageWidth', + 'asset_exif.exposureTime', + 'asset_exif.fileSizeInByte', + 'asset_exif.fNumber', + 'asset_exif.focalLength', + 'asset_exif.fps', + 'asset_exif.iso', + 'asset_exif.latitude', + 'asset_exif.lensModel', + 'asset_exif.livePhotoCID', + 'asset_exif.longitude', + 'asset_exif.make', + 'asset_exif.model', + 'asset_exif.modifyDate', + 'asset_exif.orientation', + 'asset_exif.profileDescription', + 'asset_exif.projectionType', + 'asset_exif.rating', + 'asset_exif.state', + 'asset_exif.timeZone', ], } as const; diff --git a/server/src/db.d.ts b/server/src/db.d.ts deleted file mode 100644 index 7a4c319d0b..0000000000 --- a/server/src/db.d.ts +++ /dev/null @@ -1,530 +0,0 @@ -/** - * This file was generated by kysely-codegen. - * Please do not edit it manually. - */ - -import type { ColumnType } from 'kysely'; -import { - AlbumUserRole, - AssetFileType, - AssetOrder, - AssetStatus, - AssetType, - AssetVisibility, - MemoryType, - NotificationLevel, - NotificationType, - Permission, - SharedLinkType, - SourceType, - SyncEntityType, -} from 'src/enum'; -import { UserTable } from 'src/schema/tables/user.table'; -import { OnThisDayData, UserMetadataItem } from 'src/types'; - -export type ArrayType = ArrayTypeImpl extends (infer U)[] ? U[] : ArrayTypeImpl; - -export type ArrayTypeImpl = T extends ColumnType ? ColumnType : T[]; - -export type Generated = - T extends ColumnType ? ColumnType : ColumnType; - -export type Int8 = ColumnType; - -export type Json = JsonValue; - -export type JsonArray = JsonValue[]; - -export type JsonObject = { - [x: string]: JsonValue | undefined; -}; - -export type JsonPrimitive = boolean | number | string | null; - -export type JsonValue = JsonArray | JsonObject | JsonPrimitive; - -export type Timestamp = ColumnType; - -export interface Activity { - albumId: string; - assetId: string | null; - comment: string | null; - createdAt: Generated; - id: Generated; - isLiked: Generated; - updatedAt: Generated; - updateId: Generated; - userId: string; -} - -export interface Albums { - albumName: Generated; - /** - * Asset ID to be used as thumbnail - */ - albumThumbnailAssetId: string | null; - createdAt: Generated; - deletedAt: Timestamp | null; - description: Generated; - id: Generated; - isActivityEnabled: Generated; - order: Generated; - ownerId: string; - updatedAt: Generated; - updateId: Generated; -} - -export interface AlbumsAudit { - deletedAt: Generated; - id: Generated; - albumId: string; - userId: string; -} - -export interface AlbumUsersAudit { - deletedAt: Generated; - id: Generated; - albumId: string; - userId: string; -} - -export interface AlbumsAssetsAssets { - albumsId: string; - assetsId: string; - createdAt: Generated; -} - -export interface AlbumsSharedUsersUsers { - albumsId: string; - role: Generated; - usersId: string; - createId: Generated; - createdAt: Generated; - updateId: Generated; - updatedAt: Generated; -} - -export interface ApiKeys { - createdAt: Generated; - id: Generated; - key: string; - name: string; - permissions: Permission[]; - updatedAt: Generated; - updateId: Generated; - userId: string; -} - -export interface AssetFaces { - assetId: string; - boundingBoxX1: Generated; - boundingBoxX2: Generated; - boundingBoxY1: Generated; - boundingBoxY2: Generated; - deletedAt: Timestamp | null; - id: Generated; - imageHeight: Generated; - imageWidth: Generated; - personId: string | null; - sourceType: Generated; -} - -export interface AssetFiles { - assetId: string; - createdAt: Generated; - id: Generated; - path: string; - type: AssetFileType; - updatedAt: Generated; - updateId: Generated; -} - -export interface AssetJobStatus { - assetId: string; - duplicatesDetectedAt: Timestamp | null; - facesRecognizedAt: Timestamp | null; - metadataExtractedAt: Timestamp | null; - previewAt: Timestamp | null; - thumbnailAt: Timestamp | null; -} - -export interface AssetsAudit { - deletedAt: Generated; - id: Generated; - assetId: string; - ownerId: string; -} - -export interface Assets { - checksum: Buffer; - createdAt: Generated; - deletedAt: Timestamp | null; - deviceAssetId: string; - deviceId: string; - duplicateId: string | null; - duration: string | null; - encodedVideoPath: Generated; - fileCreatedAt: Timestamp; - fileModifiedAt: Timestamp; - id: Generated; - isExternal: Generated; - isFavorite: Generated; - isOffline: Generated; - visibility: Generated; - libraryId: string | null; - livePhotoVideoId: string | null; - localDateTime: Timestamp; - originalFileName: string; - originalPath: string; - ownerId: string; - sidecarPath: string | null; - stackId: string | null; - status: Generated; - thumbhash: Buffer | null; - type: AssetType; - updatedAt: Generated; - updateId: Generated; -} - -export interface AssetStack { - id: Generated; - ownerId: string; - primaryAssetId: string; -} - -export interface Audit { - action: string; - createdAt: Generated; - entityId: string; - entityType: string; - id: Generated; - ownerId: string; -} - -export interface Exif { - assetId: string; - updateId: Generated; - updatedAt: Generated; - autoStackId: string | null; - bitsPerSample: number | null; - city: string | null; - colorspace: string | null; - country: string | null; - dateTimeOriginal: Timestamp | null; - description: Generated; - exifImageHeight: number | null; - exifImageWidth: number | null; - exposureTime: string | null; - fileSizeInByte: Int8 | null; - fNumber: number | null; - focalLength: number | null; - fps: number | null; - iso: number | null; - latitude: number | null; - lensModel: string | null; - livePhotoCID: string | null; - longitude: number | null; - make: string | null; - model: string | null; - modifyDate: Timestamp | null; - orientation: string | null; - profileDescription: string | null; - projectionType: string | null; - rating: number | null; - state: string | null; - timeZone: string | null; -} - -export interface FaceSearch { - embedding: string; - faceId: string; -} - -export interface GeodataPlaces { - admin1Code: string | null; - admin1Name: string | null; - admin2Code: string | null; - admin2Name: string | null; - alternateNames: string | null; - countryCode: string; - id: number; - latitude: number; - longitude: number; - modificationDate: Timestamp; - name: string; -} - -export interface Libraries { - createdAt: Generated; - deletedAt: Timestamp | null; - exclusionPatterns: string[]; - id: Generated; - importPaths: string[]; - name: string; - ownerId: string; - refreshedAt: Timestamp | null; - updatedAt: Generated; - updateId: Generated; -} - -export interface Memories { - createdAt: Generated; - data: OnThisDayData; - deletedAt: Timestamp | null; - hideAt: Timestamp | null; - id: Generated; - isSaved: Generated; - memoryAt: Timestamp; - ownerId: string; - seenAt: Timestamp | null; - showAt: Timestamp | null; - type: MemoryType; - updatedAt: Generated; - updateId: Generated; -} - -export interface Notifications { - id: Generated; - createdAt: Generated; - updatedAt: Generated; - deletedAt: Timestamp | null; - updateId: Generated; - userId: string; - level: Generated; - type: NotificationType; - title: string; - description: string | null; - data: any | null; - readAt: Timestamp | null; -} - -export interface MemoriesAssetsAssets { - assetsId: string; - memoriesId: string; -} - -export interface Migrations { - id: Generated; - name: string; - timestamp: Int8; -} - -export interface MoveHistory { - entityId: string; - id: Generated; - newPath: string; - oldPath: string; - pathType: string; -} - -export interface NaturalearthCountries { - admin: string; - admin_a3: string; - coordinates: string; - id: Generated; - type: string; -} - -export interface PartnersAudit { - deletedAt: Generated; - id: Generated; - sharedById: string; - sharedWithId: string; -} - -export interface Partners { - createdAt: Generated; - createId: Generated; - inTimeline: Generated; - sharedById: string; - sharedWithId: string; - updatedAt: Generated; - updateId: Generated; -} - -export interface Person { - birthDate: Timestamp | null; - color: string | null; - createdAt: Generated; - faceAssetId: string | null; - id: Generated; - isFavorite: Generated; - isHidden: Generated; - name: Generated; - ownerId: string; - thumbnailPath: Generated; - updatedAt: Generated; - updateId: Generated; -} - -export interface Sessions { - createdAt: Generated; - deviceOS: Generated; - deviceType: Generated; - id: Generated; - parentId: string | null; - expiresAt: Date | null; - token: string; - updatedAt: Generated; - updateId: Generated; - userId: string; - pinExpiresAt: Timestamp | null; -} - -export interface SessionSyncCheckpoints { - ack: string; - createdAt: Generated; - sessionId: string; - type: SyncEntityType; - updatedAt: Generated; - updateId: Generated; -} - -export interface SharedLinkAsset { - assetsId: string; - sharedLinksId: string; -} - -export interface SharedLinks { - albumId: string | null; - allowDownload: Generated; - allowUpload: Generated; - createdAt: Generated; - description: string | null; - expiresAt: Timestamp | null; - id: Generated; - key: Buffer; - password: string | null; - showExif: Generated; - type: SharedLinkType; - userId: string; -} - -export interface SmartSearch { - assetId: string; - embedding: string; -} - -export interface SocketIoAttachments { - created_at: Generated; - id: Generated; - payload: Buffer | null; -} - -export interface SystemConfig { - key: string; - value: string | null; -} - -export interface SystemMetadata { - key: string; - value: Json; -} - -export interface TagAsset { - assetsId: string; - tagsId: string; -} - -export interface Tags { - color: string | null; - createdAt: Generated; - id: Generated; - parentId: string | null; - updatedAt: Generated; - updateId: Generated; - userId: string; - value: string; -} - -export interface TagsClosure { - id_ancestor: string; - id_descendant: string; -} - -export interface TypeormMetadata { - database: string | null; - name: string | null; - schema: string | null; - table: string | null; - type: string; - value: string | null; -} - -export interface UserMetadata extends UserMetadataItem { - userId: string; -} - -export interface UsersAudit { - id: Generated; - userId: string; - deletedAt: Generated; -} - -export interface VectorsPgVectorIndexStat { - idx_growing: ArrayType | null; - idx_indexing: boolean | null; - idx_options: string | null; - idx_sealed: ArrayType | null; - idx_size: Int8 | null; - idx_status: string | null; - idx_tuples: Int8 | null; - idx_write: Int8 | null; - indexname: string | null; - indexrelid: number | null; - tablename: string | null; - tablerelid: number | null; -} - -export interface VersionHistory { - createdAt: Generated; - id: Generated; - version: string; -} - -export interface DB { - activity: Activity; - albums: Albums; - albums_audit: AlbumsAudit; - albums_assets_assets: AlbumsAssetsAssets; - albums_shared_users_users: AlbumsSharedUsersUsers; - album_users_audit: AlbumUsersAudit; - api_keys: ApiKeys; - asset_faces: AssetFaces; - asset_files: AssetFiles; - asset_job_status: AssetJobStatus; - asset_stack: AssetStack; - assets: Assets; - assets_audit: AssetsAudit; - audit: Audit; - exif: Exif; - face_search: FaceSearch; - geodata_places: GeodataPlaces; - libraries: Libraries; - memories: Memories; - memories_assets_assets: MemoriesAssetsAssets; - migrations: Migrations; - notifications: Notifications; - move_history: MoveHistory; - naturalearth_countries: NaturalearthCountries; - partners_audit: PartnersAudit; - partners: Partners; - person: Person; - sessions: Sessions; - session_sync_checkpoints: SessionSyncCheckpoints; - shared_link__asset: SharedLinkAsset; - shared_links: SharedLinks; - smart_search: SmartSearch; - socket_io_attachments: SocketIoAttachments; - system_config: SystemConfig; - system_metadata: SystemMetadata; - tag_asset: TagAsset; - tags: Tags; - tags_closure: TagsClosure; - typeorm_metadata: TypeormMetadata; - user_metadata: UserMetadata; - users: UserTable; - users_audit: UsersAudit; - 'vectors.pg_vector_index_stat': VectorsPgVectorIndexStat; - version_history: VersionHistory; -} diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 766e7c70b9..b88f2d2d7e 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -131,7 +131,7 @@ export interface GenerateSqlQueries { } export const Telemetry = (options: { enabled?: boolean }) => - SetMetadata(MetadataKey.TELEMETRY_ENABLED, options?.enabled ?? true); + SetMetadata(MetadataKey.TelemetryEnabled, options?.enabled ?? true); /** Decorator to enable versioning/tracking of generated Sql */ export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options); @@ -145,13 +145,13 @@ export type EventConfig = { /** register events for these workers, defaults to all workers */ workers?: ImmichWorker[]; }; -export const OnEvent = (config: EventConfig) => SetMetadata(MetadataKey.EVENT_CONFIG, config); +export const OnEvent = (config: EventConfig) => SetMetadata(MetadataKey.EventConfig, config); export type JobConfig = { name: JobName; queue: QueueName; }; -export const OnJob = (config: JobConfig) => SetMetadata(MetadataKey.JOB_CONFIG, config); +export const OnJob = (config: JobConfig) => SetMetadata(MetadataKey.JobConfig, config); type LifecycleRelease = 'NEXT_RELEASE' | string; type LifecycleMetadata = { diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index d11fe8da7e..4b11a16e14 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; +import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; import { Activity } from 'src/database'; import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; -import { Optional, ValidateUUID } from 'src/validation'; +import { ValidateEnum, ValidateUUID } from 'src/validation'; export enum ReactionType { COMMENT = 'comment', @@ -19,7 +19,7 @@ export type MaybeDuplicate = { duplicate: boolean; value: T }; export class ActivityResponseDto { id!: string; createdAt!: Date; - @ApiProperty({ enumName: 'ReactionType', enum: ReactionType }) + @ValidateEnum({ enum: ReactionType, name: 'ReactionType' }) type!: ReactionType; user!: UserResponseDto; assetId!: string | null; @@ -43,14 +43,10 @@ export class ActivityDto { } export class ActivitySearchDto extends ActivityDto { - @IsEnum(ReactionType) - @Optional() - @ApiProperty({ enumName: 'ReactionType', enum: ReactionType }) + @ValidateEnum({ enum: ReactionType, name: 'ReactionType', optional: true }) type?: ReactionType; - @IsEnum(ReactionLevel) - @Optional() - @ApiProperty({ enumName: 'ReactionLevel', enum: ReactionLevel }) + @ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', optional: true }) level?: ReactionLevel; @ValidateUUID({ optional: true }) @@ -60,8 +56,7 @@ export class ActivitySearchDto extends ActivityDto { const isComment = (dto: ActivityCreateDto) => dto.type === ReactionType.COMMENT; export class ActivityCreateDto extends ActivityDto { - @IsEnum(ReactionType) - @ApiProperty({ enumName: 'ReactionType', enum: ReactionType }) + @ValidateEnum({ enum: ReactionType, name: 'ReactionType' }) type!: ReactionType; @ValidateIf(isComment) diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 40e51ef729..3a88ba5be3 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -1,13 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, IsEnum, IsString, ValidateNested } from 'class-validator'; +import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator'; import _ from 'lodash'; import { AlbumUser, AuthSharedLink, User } from 'src/database'; import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { AlbumUserRole, AssetOrder } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; export class AlbumInfoDto { @ValidateBoolean({ optional: true }) @@ -18,8 +18,7 @@ export class AlbumUserAddDto { @ValidateUUID() userId!: string; - @IsEnum(AlbumUserRole) - @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole', default: AlbumUserRole.EDITOR }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', default: AlbumUserRole.Editor }) role?: AlbumUserRole; } @@ -32,8 +31,7 @@ export class AlbumUserCreateDto { @ValidateUUID() userId!: string; - @IsEnum(AlbumUserRole) - @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' }) role!: AlbumUserRole; } @@ -71,9 +69,7 @@ export class UpdateAlbumDto { @ValidateBoolean({ optional: true }) isActivityEnabled?: boolean; - @IsEnum(AssetOrder) - @Optional() - @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true }) order?: AssetOrder; } @@ -107,14 +103,13 @@ export class AlbumStatisticsResponseDto { } export class UpdateAlbumUserDto { - @IsEnum(AlbumUserRole) - @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' }) role!: AlbumUserRole; } export class AlbumUserResponseDto { user!: UserResponseDto; - @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' }) role!: AlbumUserRole; } @@ -137,8 +132,7 @@ export class AlbumResponseDto { startDate?: Date; endDate?: Date; isActivityEnabled!: boolean; - @Optional() - @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true }) order?: AssetOrder; } diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index c790ea613d..c9475fa2b1 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -1,15 +1,13 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMinSize, IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { ArrayMinSize, IsNotEmpty, IsString } from 'class-validator'; import { Permission } from 'src/enum'; -import { Optional } from 'src/validation'; +import { Optional, ValidateEnum } from 'src/validation'; export class APIKeyCreateDto { @IsString() @IsNotEmpty() @Optional() name?: string; - @IsEnum(Permission, { each: true }) - @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) + @ValidateEnum({ enum: Permission, name: 'Permission', each: true }) @ArrayMinSize(1) permissions!: Permission[]; } @@ -20,9 +18,7 @@ export class APIKeyUpdateDto { @IsNotEmpty() name?: string; - @Optional() - @IsEnum(Permission, { each: true }) - @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) + @ValidateEnum({ enum: Permission, name: 'Permission', each: true, optional: true }) @ArrayMinSize(1) permissions?: Permission[]; } @@ -37,6 +33,6 @@ export class APIKeyResponseDto { name!: string; createdAt!: Date; updatedAt!: Date; - @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) + @ValidateEnum({ enum: Permission, name: 'Permission', each: true }) permissions!: Permission[]; } diff --git a/server/src/dtos/asset-media-response.dto.ts b/server/src/dtos/asset-media-response.dto.ts index 5cd9b7e7d9..887762dbdd 100644 --- a/server/src/dtos/asset-media-response.dto.ts +++ b/server/src/dtos/asset-media-response.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ValidateEnum } from 'src/validation'; export enum AssetMediaStatus { CREATED = 'created', @@ -6,7 +6,7 @@ export enum AssetMediaStatus { DUPLICATE = 'duplicate', } export class AssetMediaResponseDto { - @ApiProperty({ enum: AssetMediaStatus, enumName: 'AssetMediaStatus' }) + @ValidateEnum({ enum: AssetMediaStatus, name: 'AssetMediaStatus' }) status!: AssetMediaStatus; id!: string; } diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index 92e1302864..ea86e087d8 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, IsEnum, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; +import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; import { AssetVisibility } from 'src/enum'; -import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; export enum AssetMediaSize { /** @@ -15,9 +15,7 @@ export enum AssetMediaSize { } export class AssetMediaOptionsDto { - @Optional() - @IsEnum(AssetMediaSize) - @ApiProperty({ enumName: 'AssetMediaSize', enum: AssetMediaSize }) + @ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', optional: true }) size?: AssetMediaSize; } @@ -60,7 +58,7 @@ export class AssetMediaCreateDto extends AssetMediaBase { @ValidateBoolean({ optional: true }) isFavorite?: boolean; - @ValidateAssetVisibility({ optional: true }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true }) visibility?: AssetVisibility; @ValidateUUID({ optional: true }) diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 1e214c3860..98ed8669f0 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -15,10 +15,11 @@ import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { mimeTypes } from 'src/utils/mime-types'; +import { ValidateEnum } from 'src/validation'; export class SanitizedAssetResponseDto { id!: string; - @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) + @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum' }) type!: AssetType; thumbhash!: string | null; originalMimeType?: string; @@ -72,7 +73,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { isArchived!: boolean; isTrashed!: boolean; isOffline!: boolean; - @ApiProperty({ enum: AssetVisibility, enumName: 'AssetVisibility' }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility' }) visibility!: AssetVisibility; exifInfo?: ExifResponseDto; tags?: TagResponseDto[]; @@ -204,7 +205,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset localDateTime: entity.localDateTime, updatedAt: entity.updatedAt, isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false, - isArchived: entity.visibility === AssetVisibility.ARCHIVE, + isArchived: entity.visibility === AssetVisibility.Archive, isTrashed: !!entity.deletedAt, visibility: entity.visibility, duration: entity.duration ?? '0:00:00.00000', diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index 940cfbf9cc..5728d21646 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -2,7 +2,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsDateString, - IsEnum, IsInt, IsLatitude, IsLongitude, @@ -16,7 +15,7 @@ import { import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AssetType, AssetVisibility } from 'src/enum'; import { AssetStats } from 'src/repositories/asset.repository'; -import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; export class DeviceIdDto { @IsNotEmpty() @@ -32,7 +31,7 @@ export class UpdateAssetBase { @ValidateBoolean({ optional: true }) isFavorite?: boolean; - @ValidateAssetVisibility({ optional: true }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true }) visibility?: AssetVisibility; @Optional() @@ -99,13 +98,12 @@ export enum AssetJobName { } export class AssetJobsDto extends AssetIdsDto { - @ApiProperty({ enumName: 'AssetJobName', enum: AssetJobName }) - @IsEnum(AssetJobName) + @ValidateEnum({ enum: AssetJobName, name: 'AssetJobName' }) name!: AssetJobName; } export class AssetStatsDto { - @ValidateAssetVisibility({ optional: true }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true }) visibility?: AssetVisibility; @ValidateBoolean({ optional: true }) @@ -128,8 +126,8 @@ export class AssetStatsResponseDto { export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { return { - images: stats[AssetType.IMAGE], - videos: stats[AssetType.VIDEO], + images: stats[AssetType.Image], + videos: stats[AssetType.Video], total: Object.values(stats).reduce((total, value) => total + value, 0), }; }; diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index e94818b2b5..2bb98b34a5 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -45,7 +45,7 @@ export class LoginResponseDto { export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto { const onboardingMetadata = entity.metadata.find( - (item): item is UserMetadataItem => item.key === UserMetadataKey.ONBOARDING, + (item): item is UserMetadataItem => item.key === UserMetadataKey.Onboarding, )?.value; return { diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts index 99fd1d2149..3543d8dae9 100644 --- a/server/src/dtos/env.dto.ts +++ b/server/src/dtos/env.dto.ts @@ -1,5 +1,5 @@ import { Transform, Type } from 'class-transformer'; -import { IsEnum, IsInt, IsString } from 'class-validator'; +import { IsEnum, IsInt, IsString, Matches } from 'class-validator'; import { DatabaseSslMode, ImmichEnvironment, LogLevel } from 'src/enum'; import { IsIPRange, Optional, ValidateBoolean } from 'src/validation'; @@ -48,6 +48,10 @@ export class EnvDto { @Optional() IMMICH_LOG_LEVEL?: LogLevel; + @Optional() + @Matches(/^\//, { message: 'IMMICH_MEDIA_LOCATION must be an absolute path' }) + IMMICH_MEDIA_LOCATION?: string; + @IsInt() @Optional() @Type(() => Number) diff --git a/server/src/dtos/job.dto.ts b/server/src/dtos/job.dto.ts index ce6aad4c06..2123b65878 100644 --- a/server/src/dtos/job.dto.ts +++ b/server/src/dtos/job.dto.ts @@ -1,19 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty } from 'class-validator'; import { JobCommand, ManualJobName, QueueName } from 'src/enum'; -import { ValidateBoolean } from 'src/validation'; +import { ValidateBoolean, ValidateEnum } from 'src/validation'; export class JobIdParamDto { - @IsNotEmpty() - @IsEnum(QueueName) - @ApiProperty({ type: String, enum: QueueName, enumName: 'JobName' }) + @ValidateEnum({ enum: QueueName, name: 'JobName' }) id!: QueueName; } export class JobCommandDto { - @IsNotEmpty() - @IsEnum(JobCommand) - @ApiProperty({ type: 'string', enum: JobCommand, enumName: 'JobCommand' }) + @ValidateEnum({ enum: JobCommand, name: 'JobCommand' }) command!: JobCommand; @ValidateBoolean({ optional: true }) @@ -21,8 +16,7 @@ export class JobCommandDto { } export class JobCreateDto { - @IsEnum(ManualJobName) - @ApiProperty({ type: 'string', enum: ManualJobName, enumName: 'ManualJobName' }) + @ValidateEnum({ enum: ManualJobName, name: 'ManualJobName' }) name!: ManualJobName; } @@ -56,47 +50,47 @@ export class JobStatusDto { export class AllJobStatusResponseDto implements Record { @ApiProperty({ type: JobStatusDto }) - [QueueName.THUMBNAIL_GENERATION]!: JobStatusDto; + [QueueName.ThumbnailGeneration]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.METADATA_EXTRACTION]!: JobStatusDto; + [QueueName.MetadataExtraction]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.VIDEO_CONVERSION]!: JobStatusDto; + [QueueName.VideoConversion]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.SMART_SEARCH]!: JobStatusDto; + [QueueName.SmartSearch]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.STORAGE_TEMPLATE_MIGRATION]!: JobStatusDto; + [QueueName.StorageTemplateMigration]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.MIGRATION]!: JobStatusDto; + [QueueName.Migration]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.BACKGROUND_TASK]!: JobStatusDto; + [QueueName.BackgroundTask]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.SEARCH]!: JobStatusDto; + [QueueName.Search]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.DUPLICATE_DETECTION]!: JobStatusDto; + [QueueName.DuplicateDetection]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.FACE_DETECTION]!: JobStatusDto; + [QueueName.FaceDetection]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.FACIAL_RECOGNITION]!: JobStatusDto; + [QueueName.FacialRecognition]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.SIDECAR]!: JobStatusDto; + [QueueName.Sidecar]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.LIBRARY]!: JobStatusDto; + [QueueName.Library]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.NOTIFICATION]!: JobStatusDto; + [QueueName.Notification]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.BACKUP_DATABASE]!: JobStatusDto; + [QueueName.BackupDatabase]!: JobStatusDto; } diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index 675039363b..a79511c73e 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -1,11 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; +import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; import { Memory } from 'src/database'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; class MemoryBaseDto { @ValidateBoolean({ optional: true }) @@ -16,9 +16,7 @@ class MemoryBaseDto { } export class MemorySearchDto { - @Optional() - @IsEnum(MemoryType) - @ApiProperty({ enum: MemoryType, enumName: 'MemoryType' }) + @ValidateEnum({ enum: MemoryType, name: 'MemoryType', optional: true }) type?: MemoryType; @ValidateDate({ optional: true }) @@ -45,15 +43,14 @@ export class MemoryUpdateDto extends MemoryBaseDto { } export class MemoryCreateDto extends MemoryBaseDto { - @IsEnum(MemoryType) - @ApiProperty({ enum: MemoryType, enumName: 'MemoryType' }) + @ValidateEnum({ enum: MemoryType, name: 'MemoryType' }) type!: MemoryType; @IsObject() @ValidateNested() @Type((options) => { switch (options?.object.type) { - case MemoryType.ON_THIS_DAY: { + case MemoryType.OnThisDay: { return OnThisDayDto; } @@ -86,7 +83,7 @@ export class MemoryResponseDto { showAt?: Date; hideAt?: Date; ownerId!: string; - @ApiProperty({ enumName: 'MemoryType', enum: MemoryType }) + @ValidateEnum({ enum: MemoryType, name: 'MemoryType' }) type!: MemoryType; data!: MemoryData; isSaved!: boolean; diff --git a/server/src/dtos/notification.dto.ts b/server/src/dtos/notification.dto.ts index d9847cda17..e83ba7315f 100644 --- a/server/src/dtos/notification.dto.ts +++ b/server/src/dtos/notification.dto.ts @@ -1,7 +1,6 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsString } from 'class-validator'; +import { IsString } from 'class-validator'; import { NotificationLevel, NotificationType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; export class TestEmailResponseDto { messageId!: string; @@ -19,9 +18,9 @@ export class NotificationDto { id!: string; @ValidateDate() createdAt!: Date; - @ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' }) + @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel' }) level!: NotificationLevel; - @ApiProperty({ enum: NotificationType, enumName: 'NotificationType' }) + @ValidateEnum({ enum: NotificationType, name: 'NotificationType' }) type!: NotificationType; title!: string; description?: string; @@ -30,18 +29,13 @@ export class NotificationDto { } export class NotificationSearchDto { - @Optional() @ValidateUUID({ optional: true }) id?: string; - @IsEnum(NotificationLevel) - @Optional() - @ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' }) + @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', optional: true }) level?: NotificationLevel; - @IsEnum(NotificationType) - @Optional() - @ApiProperty({ enum: NotificationType, enumName: 'NotificationType' }) + @ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true }) type?: NotificationType; @ValidateBoolean({ optional: true }) @@ -49,14 +43,10 @@ export class NotificationSearchDto { } export class NotificationCreateDto { - @Optional() - @IsEnum(NotificationLevel) - @ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' }) + @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', optional: true }) level?: NotificationLevel; - @IsEnum(NotificationType) - @Optional() - @ApiProperty({ enum: NotificationType, enumName: 'NotificationType' }) + @ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true }) type?: NotificationType; @IsString() diff --git a/server/src/dtos/onboarding.dto.ts b/server/src/dtos/onboarding.dto.ts index 0028fca006..47a3992784 100644 --- a/server/src/dtos/onboarding.dto.ts +++ b/server/src/dtos/onboarding.dto.ts @@ -1,8 +1,7 @@ -import { IsBoolean, IsNotEmpty } from 'class-validator'; +import { ValidateBoolean } from 'src/validation'; export class OnboardingDto { - @IsBoolean() - @IsNotEmpty() + @ValidateBoolean() isOnboarded!: boolean; } diff --git a/server/src/dtos/partner.dto.ts b/server/src/dtos/partner.dto.ts index 9d86415dc3..28d4adf8b7 100644 --- a/server/src/dtos/partner.dto.ts +++ b/server/src/dtos/partner.dto.ts @@ -1,7 +1,7 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty } from 'class-validator'; +import { IsNotEmpty } from 'class-validator'; import { UserResponseDto } from 'src/dtos/user.dto'; import { PartnerDirection } from 'src/repositories/partner.repository'; +import { ValidateEnum } from 'src/validation'; export class UpdatePartnerDto { @IsNotEmpty() @@ -9,8 +9,7 @@ export class UpdatePartnerDto { } export class PartnerSearchDto { - @IsEnum(PartnerDirection) - @ApiProperty({ enum: PartnerDirection, enumName: 'PartnerDirection' }) + @ValidateEnum({ enum: PartnerDirection, name: 'PartnerDirection' }) direction!: PartnerDirection; } diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index c59ab905bd..f9b41627d9 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -4,16 +4,17 @@ import { IsArray, IsInt, IsNotEmpty, IsNumber, IsString, Max, Min, ValidateNeste import { Selectable } from 'kysely'; import { DateTime } from 'luxon'; import { AssetFace, Person } from 'src/database'; -import { AssetFaces } from 'src/db'; import { PropertyLifecycle } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { SourceType } from 'src/enum'; +import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { asDateString } from 'src/utils/date'; import { IsDateStringFormat, MaxDateString, Optional, ValidateBoolean, + ValidateEnum, ValidateHexColor, ValidateUUID, } from 'src/validation'; @@ -137,7 +138,7 @@ export class AssetFaceWithoutPersonResponseDto { boundingBoxY1!: number; @ApiProperty({ type: 'integer' }) boundingBoxY2!: number; - @ApiProperty({ enum: SourceType, enumName: 'SourceType' }) + @ValidateEnum({ enum: SourceType, name: 'SourceType' }) sourceType?: SourceType; } @@ -232,7 +233,7 @@ export function mapPerson(person: Person): PersonResponseDto { }; } -export function mapFacesWithoutPerson(face: Selectable): AssetFaceWithoutPersonResponseDto { +export function mapFacesWithoutPerson(face: Selectable): AssetFaceWithoutPersonResponseDto { return { id: face.id, imageHeight: face.imageHeight, diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index d0427ef322..aef78e51ea 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -1,12 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; +import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; import { Place } from 'src/database'; import { PropertyLifecycle } from 'src/decorators'; import { AlbumResponseDto } from 'src/dtos/album.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetOrder, AssetType, AssetVisibility } from 'src/enum'; -import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; class BaseSearchDto { @ValidateUUID({ optional: true, nullable: true }) @@ -17,9 +17,7 @@ class BaseSearchDto { @Optional() deviceId?: string; - @IsEnum(AssetType) - @Optional() - @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) + @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', optional: true }) type?: AssetType; @ValidateBoolean({ optional: true }) @@ -34,7 +32,7 @@ class BaseSearchDto { @ValidateBoolean({ optional: true }) isOffline?: boolean; - @ValidateAssetVisibility({ optional: true }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true }) visibility?: AssetVisibility; @ValidateDate({ optional: true }) @@ -92,8 +90,8 @@ class BaseSearchDto { @ValidateUUID({ each: true, optional: true }) personIds?: string[]; - @ValidateUUID({ each: true, optional: true }) - tagIds?: string[]; + @ValidateUUID({ each: true, optional: true, nullable: true }) + tagIds?: string[] | null; @ValidateUUID({ each: true, optional: true }) albumIds?: string[]; @@ -172,9 +170,7 @@ export class MetadataSearchDto extends RandomSearchDto { @Optional() encodedVideoPath?: string; - @IsEnum(AssetOrder) - @Optional() - @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder, default: AssetOrder.DESC }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, default: AssetOrder.Desc }) order?: AssetOrder; @IsInt() @@ -250,9 +246,7 @@ export enum SearchSuggestionType { } export class SearchSuggestionRequestDto { - @IsEnum(SearchSuggestionType) - @IsNotEmpty() - @ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType }) + @ValidateEnum({ enum: SearchSuggestionType, name: 'SearchSuggestionType' }) type!: SearchSuggestionType; @IsString() diff --git a/server/src/dtos/session.dto.ts b/server/src/dtos/session.dto.ts index f15166fbf5..0babbb9182 100644 --- a/server/src/dtos/session.dto.ts +++ b/server/src/dtos/session.dto.ts @@ -1,6 +1,6 @@ import { IsInt, IsPositive, IsString } from 'class-validator'; import { Session } from 'src/database'; -import { Optional } from 'src/validation'; +import { Optional, ValidateBoolean } from 'src/validation'; export class SessionCreateDto { /** @@ -20,6 +20,11 @@ export class SessionCreateDto { deviceOS?: string; } +export class SessionUpdateDto { + @ValidateBoolean({ optional: true }) + isPendingSyncReset?: boolean; +} + export class SessionResponseDto { id!: string; createdAt!: string; @@ -28,6 +33,7 @@ export class SessionResponseDto { current!: boolean; deviceType!: string; deviceOS!: string; + isPendingSyncReset!: boolean; } export class SessionCreateResponseDto extends SessionResponseDto { @@ -42,4 +48,5 @@ export const mapSession = (entity: Session, currentId?: string): SessionResponse current: currentId === entity.id, deviceOS: entity.deviceOS, deviceType: entity.deviceType, + isPendingSyncReset: entity.isPendingSyncReset, }); diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index 8d373b40b6..299590c0e3 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,11 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsString } from 'class-validator'; +import { IsString } from 'class-validator'; import _ from 'lodash'; import { SharedLink } from 'src/database'; import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { SharedLinkType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; export class SharedLinkSearchDto { @ValidateUUID({ optional: true }) @@ -13,8 +13,7 @@ export class SharedLinkSearchDto { } export class SharedLinkCreateDto { - @IsEnum(SharedLinkType) - @ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' }) + @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType' }) type!: SharedLinkType; @ValidateUUID({ each: true, optional: true }) @@ -90,7 +89,7 @@ export class SharedLinkResponseDto { userId!: string; key!: string; - @ApiProperty({ enumName: 'SharedLinkType', enum: SharedLinkType }) + @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType' }) type!: SharedLinkType; createdAt!: Date; expiresAt!: Date | null; diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 91c93fef66..92aea8f5e9 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -1,8 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsInt, IsPositive, IsString } from 'class-validator'; +import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { AlbumUserRole, AssetOrder, AssetType, AssetVisibility, SyncEntityType, SyncRequestType } from 'src/enum'; -import { Optional, ValidateDate, ValidateUUID } from 'src/validation'; +import { + AlbumUserRole, + AssetOrder, + AssetType, + AssetVisibility, + MemoryType, + SyncEntityType, + SyncRequestType, + UserAvatarColor, + UserMetadataKey, +} from 'src/enum'; +import { UserMetadata } from 'src/types'; +import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; export class AssetFullSyncDto { @ValidateUUID({ optional: true }) @@ -34,28 +46,57 @@ export class AssetDeltaSyncResponseDto { deleted!: string[]; } +export const extraSyncModels: Function[] = []; + +export const ExtraModel = (): ClassDecorator => { + return (object: Function) => { + extraSyncModels.push(object); + }; +}; + +@ExtraModel() export class SyncUserV1 { id!: string; name!: string; email!: string; + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', nullable: true }) + avatarColor!: UserAvatarColor | null; deletedAt!: Date | null; } +@ExtraModel() +export class SyncAuthUserV1 extends SyncUserV1 { + isAdmin!: boolean; + pinCode!: string | null; + oauthId!: string; + storageLabel!: string | null; + @ApiProperty({ type: 'integer' }) + quotaSizeInBytes!: number | null; + @ApiProperty({ type: 'integer' }) + quotaUsageInBytes!: number; + hasProfileImage!: boolean; + profileChangedAt!: Date; +} + +@ExtraModel() export class SyncUserDeleteV1 { userId!: string; } +@ExtraModel() export class SyncPartnerV1 { sharedById!: string; sharedWithId!: string; inTimeline!: boolean; } +@ExtraModel() export class SyncPartnerDeleteV1 { sharedById!: string; sharedWithId!: string; } +@ExtraModel() export class SyncAssetV1 { id!: string; ownerId!: string; @@ -66,18 +107,22 @@ export class SyncAssetV1 { fileModifiedAt!: Date | null; localDateTime!: Date | null; duration!: string | null; - @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) + @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum' }) type!: AssetType; deletedAt!: Date | null; isFavorite!: boolean; - @ApiProperty({ enumName: 'AssetVisibility', enum: AssetVisibility }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility' }) visibility!: AssetVisibility; + livePhotoVideoId!: string | null; + stackId!: string | null; } +@ExtraModel() export class SyncAssetDeleteV1 { assetId!: string; } +@ExtraModel() export class SyncAssetExifV1 { assetId!: string; description!: string | null; @@ -91,9 +136,9 @@ export class SyncAssetExifV1 { dateTimeOriginal!: Date | null; modifyDate!: Date | null; timeZone!: string | null; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'number', format: 'double' }) latitude!: number | null; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'number', format: 'double' }) longitude!: number | null; projectionType!: string | null; city!: string | null; @@ -102,9 +147,9 @@ export class SyncAssetExifV1 { make!: string | null; model!: string | null; lensModel!: string | null; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'number', format: 'double' }) fNumber!: number | null; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'number', format: 'double' }) focalLength!: number | null; @ApiProperty({ type: 'integer' }) iso!: number | null; @@ -112,26 +157,30 @@ export class SyncAssetExifV1 { profileDescription!: string | null; @ApiProperty({ type: 'integer' }) rating!: number | null; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'number', format: 'double' }) fps!: number | null; } +@ExtraModel() export class SyncAlbumDeleteV1 { albumId!: string; } +@ExtraModel() export class SyncAlbumUserDeleteV1 { albumId!: string; userId!: string; } +@ExtraModel() export class SyncAlbumUserV1 { albumId!: string; userId!: string; - @ApiProperty({ enumName: 'AlbumUserRole', enum: AlbumUserRole }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' }) role!: AlbumUserRole; } +@ExtraModel() export class SyncAlbumV1 { id!: string; ownerId!: string; @@ -141,11 +190,137 @@ export class SyncAlbumV1 { updatedAt!: Date; thumbnailAssetId!: string | null; isActivityEnabled!: boolean; - @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' }) order!: AssetOrder; } +@ExtraModel() +export class SyncAlbumToAssetV1 { + albumId!: string; + assetId!: string; +} + +@ExtraModel() +export class SyncAlbumToAssetDeleteV1 { + albumId!: string; + assetId!: string; +} + +@ExtraModel() +export class SyncMemoryV1 { + id!: string; + createdAt!: Date; + updatedAt!: Date; + deletedAt!: Date | null; + ownerId!: string; + @ValidateEnum({ enum: MemoryType, name: 'MemoryType' }) + type!: MemoryType; + data!: object; + isSaved!: boolean; + memoryAt!: Date; + seenAt!: Date | null; + showAt!: Date | null; + hideAt!: Date | null; +} + +@ExtraModel() +export class SyncMemoryDeleteV1 { + memoryId!: string; +} + +@ExtraModel() +export class SyncMemoryAssetV1 { + memoryId!: string; + assetId!: string; +} + +@ExtraModel() +export class SyncMemoryAssetDeleteV1 { + memoryId!: string; + assetId!: string; +} + +@ExtraModel() +export class SyncStackV1 { + id!: string; + createdAt!: Date; + updatedAt!: Date; + primaryAssetId!: string; + ownerId!: string; +} + +@ExtraModel() +export class SyncStackDeleteV1 { + stackId!: string; +} + +@ExtraModel() +export class SyncPersonV1 { + id!: string; + createdAt!: Date; + updatedAt!: Date; + ownerId!: string; + name!: string; + birthDate!: Date | null; + isHidden!: boolean; + isFavorite!: boolean; + color!: string | null; + faceAssetId!: string | null; +} + +@ExtraModel() +export class SyncPersonDeleteV1 { + personId!: string; +} + +@ExtraModel() +export class SyncAssetFaceV1 { + id!: string; + assetId!: string; + personId!: string | null; + @ApiProperty({ type: 'integer' }) + imageWidth!: number; + @ApiProperty({ type: 'integer' }) + imageHeight!: number; + @ApiProperty({ type: 'integer' }) + boundingBoxX1!: number; + @ApiProperty({ type: 'integer' }) + boundingBoxY1!: number; + @ApiProperty({ type: 'integer' }) + boundingBoxX2!: number; + @ApiProperty({ type: 'integer' }) + boundingBoxY2!: number; + sourceType!: string; +} + +@ExtraModel() +export class SyncAssetFaceDeleteV1 { + assetFaceId!: string; +} + +@ExtraModel() +export class SyncUserMetadataV1 { + userId!: string; + @ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey' }) + key!: UserMetadataKey; + value!: UserMetadata[UserMetadataKey]; +} + +@ExtraModel() +export class SyncUserMetadataDeleteV1 { + userId!: string; + @ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey' }) + key!: UserMetadataKey; +} + +@ExtraModel() +export class SyncAckV1 {} + +@ExtraModel() +export class SyncResetV1 {} + export type SyncItem = { + [SyncEntityType.AuthUserV1]: SyncAuthUserV1; [SyncEntityType.UserV1]: SyncUserV1; [SyncEntityType.UserDeleteV1]: SyncUserDeleteV1; [SyncEntityType.PartnerV1]: SyncPartnerV1; @@ -163,45 +338,53 @@ export type SyncItem = { [SyncEntityType.AlbumUserV1]: SyncAlbumUserV1; [SyncEntityType.AlbumUserBackfillV1]: SyncAlbumUserV1; [SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1; - [SyncEntityType.SyncAckV1]: object; + [SyncEntityType.AlbumAssetV1]: SyncAssetV1; + [SyncEntityType.AlbumAssetBackfillV1]: SyncAssetV1; + [SyncEntityType.AlbumAssetExifV1]: SyncAssetExifV1; + [SyncEntityType.AlbumAssetExifBackfillV1]: SyncAssetExifV1; + [SyncEntityType.AlbumToAssetV1]: SyncAlbumToAssetV1; + [SyncEntityType.AlbumToAssetBackfillV1]: SyncAlbumToAssetV1; + [SyncEntityType.AlbumToAssetDeleteV1]: SyncAlbumToAssetDeleteV1; + [SyncEntityType.MemoryV1]: SyncMemoryV1; + [SyncEntityType.MemoryDeleteV1]: SyncMemoryDeleteV1; + [SyncEntityType.MemoryToAssetV1]: SyncMemoryAssetV1; + [SyncEntityType.MemoryToAssetDeleteV1]: SyncMemoryAssetDeleteV1; + [SyncEntityType.StackV1]: SyncStackV1; + [SyncEntityType.StackDeleteV1]: SyncStackDeleteV1; + [SyncEntityType.PartnerStackBackfillV1]: SyncStackV1; + [SyncEntityType.PartnerStackDeleteV1]: SyncStackDeleteV1; + [SyncEntityType.PartnerStackV1]: SyncStackV1; + [SyncEntityType.PersonV1]: SyncPersonV1; + [SyncEntityType.PersonDeleteV1]: SyncPersonDeleteV1; + [SyncEntityType.AssetFaceV1]: SyncAssetFaceV1; + [SyncEntityType.AssetFaceDeleteV1]: SyncAssetFaceDeleteV1; + [SyncEntityType.UserMetadataV1]: SyncUserMetadataV1; + [SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1; + [SyncEntityType.SyncAckV1]: SyncAckV1; + [SyncEntityType.SyncResetV1]: SyncResetV1; }; -const responseDtos = [ - SyncUserV1, - SyncUserDeleteV1, - SyncPartnerV1, - SyncPartnerDeleteV1, - SyncAssetV1, - SyncAssetDeleteV1, - SyncAssetExifV1, - SyncAlbumV1, - SyncAlbumDeleteV1, - SyncAlbumUserV1, - SyncAlbumUserDeleteV1, -]; - -export const extraSyncModels = responseDtos; - export class SyncStreamDto { - @IsEnum(SyncRequestType, { each: true }) - @ApiProperty({ enumName: 'SyncRequestType', enum: SyncRequestType, isArray: true }) + @ValidateEnum({ enum: SyncRequestType, name: 'SyncRequestType', each: true }) types!: SyncRequestType[]; + + @ValidateBoolean({ optional: true }) + reset?: boolean; } export class SyncAckDto { - @ApiProperty({ enumName: 'SyncEntityType', enum: SyncEntityType }) + @ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType' }) type!: SyncEntityType; ack!: string; } export class SyncAckSetDto { + @ArrayMaxSize(1000) @IsString({ each: true }) acks!: string[]; } export class SyncAckDeleteDto { - @IsEnum(SyncEntityType, { each: true }) - @ApiProperty({ enumName: 'SyncEntityType', enum: SyncEntityType, isArray: true }) - @Optional() + @ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType', optional: true, each: true }) types?: SyncEntityType[]; } diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 03ef9192db..8a58995de7 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -2,8 +2,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { Exclude, Transform, Type } from 'class-transformer'; import { ArrayMinSize, - IsBoolean, - IsEnum, IsInt, IsNotEmpty, IsNumber, @@ -28,13 +26,13 @@ import { OAuthTokenEndpointAuthMethod, QueueName, ToneMapping, - TranscodeHWAccel, + TranscodeHardwareAcceleration, TranscodePolicy, VideoCodec, VideoContainer, } from 'src/enum'; import { ConcurrentQueueName } from 'src/types'; -import { IsCronExpression, Optional, ValidateBoolean } from 'src/validation'; +import { IsCronExpression, IsDateStringFormat, Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled; @@ -82,24 +80,19 @@ export class SystemConfigFFmpegDto { @IsString() preset!: string; - @IsEnum(VideoCodec) - @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec }) + @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec' }) targetVideoCodec!: VideoCodec; - @IsEnum(VideoCodec, { each: true }) - @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec, isArray: true }) + @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', each: true }) acceptedVideoCodecs!: VideoCodec[]; - @IsEnum(AudioCodec) - @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec }) + @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec' }) targetAudioCodec!: AudioCodec; - @IsEnum(AudioCodec, { each: true }) - @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true }) + @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', each: true }) acceptedAudioCodecs!: AudioCodec[]; - @IsEnum(VideoContainer, { each: true }) - @ApiProperty({ enumName: 'VideoContainer', enum: VideoContainer, isArray: true }) + @ValidateEnum({ enum: VideoContainer, name: 'VideoContainer', each: true }) acceptedContainers!: VideoContainer[]; @IsString() @@ -131,8 +124,7 @@ export class SystemConfigFFmpegDto { @ValidateBoolean() temporalAQ!: boolean; - @IsEnum(CQMode) - @ApiProperty({ enumName: 'CQMode', enum: CQMode }) + @ValidateEnum({ enum: CQMode, name: 'CQMode' }) cqMode!: CQMode; @ValidateBoolean() @@ -141,19 +133,16 @@ export class SystemConfigFFmpegDto { @IsString() preferredHwDevice!: string; - @IsEnum(TranscodePolicy) - @ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy }) + @ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy' }) transcode!: TranscodePolicy; - @IsEnum(TranscodeHWAccel) - @ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel }) - accel!: TranscodeHWAccel; + @ValidateEnum({ enum: TranscodeHardwareAcceleration, name: 'TranscodeHWAccel' }) + accel!: TranscodeHardwareAcceleration; @ValidateBoolean() accelDecode!: boolean; - @IsEnum(ToneMapping) - @ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping }) + @ValidateEnum({ enum: ToneMapping, name: 'ToneMapping' }) tonemap!: ToneMapping; } @@ -169,67 +158,67 @@ class SystemConfigJobDto implements Record @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.THUMBNAIL_GENERATION]!: JobSettingsDto; + [QueueName.ThumbnailGeneration]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.METADATA_EXTRACTION]!: JobSettingsDto; + [QueueName.MetadataExtraction]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.VIDEO_CONVERSION]!: JobSettingsDto; + [QueueName.VideoConversion]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.SMART_SEARCH]!: JobSettingsDto; + [QueueName.SmartSearch]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.MIGRATION]!: JobSettingsDto; + [QueueName.Migration]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.BACKGROUND_TASK]!: JobSettingsDto; + [QueueName.BackgroundTask]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.SEARCH]!: JobSettingsDto; + [QueueName.Search]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.FACE_DETECTION]!: JobSettingsDto; + [QueueName.FaceDetection]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.SIDECAR]!: JobSettingsDto; + [QueueName.Sidecar]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.LIBRARY]!: JobSettingsDto; + [QueueName.Library]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.NOTIFICATION]!: JobSettingsDto; + [QueueName.Notification]!: JobSettingsDto; } class SystemConfigLibraryScanDto { @@ -264,8 +253,7 @@ class SystemConfigLoggingDto { @ValidateBoolean() enabled!: boolean; - @ApiProperty({ enum: LogLevel, enumName: 'LogLevel' }) - @IsEnum(LogLevel) + @ValidateEnum({ enum: LogLevel, name: 'LogLevel' }) level!: LogLevel; } @@ -306,8 +294,7 @@ enum MapTheme { } export class MapThemeDto { - @IsEnum(MapTheme) - @ApiProperty({ enum: MapTheme, enumName: 'MapTheme' }) + @ValidateEnum({ enum: MapTheme, name: 'MapTheme' }) theme!: MapTheme; } @@ -329,6 +316,26 @@ class SystemConfigNewVersionCheckDto { enabled!: boolean; } +class SystemConfigNightlyTasksDto { + @IsDateStringFormat('HH:mm', { message: 'startTime must be in HH:mm format' }) + startTime!: string; + + @ValidateBoolean() + databaseCleanup!: boolean; + + @ValidateBoolean() + missingThumbnails!: boolean; + + @ValidateBoolean() + clusterNewFaces!: boolean; + + @ValidateBoolean() + generateMemories!: boolean; + + @ValidateBoolean() + syncQuotaUsage!: boolean; +} + class SystemConfigOAuthDto { @ValidateBoolean() autoLaunch!: boolean; @@ -348,8 +355,7 @@ class SystemConfigOAuthDto { @IsString() clientSecret!: string; - @IsEnum(OAuthTokenEndpointAuthMethod) - @ApiProperty({ enum: OAuthTokenEndpointAuthMethod, enumName: 'OAuthTokenEndpointAuthMethod' }) + @ValidateEnum({ enum: OAuthTokenEndpointAuthMethod, name: 'OAuthTokenEndpointAuthMethod' }) tokenEndpointAuthMethod!: OAuthTokenEndpointAuthMethod; @IsInt() @@ -395,6 +401,9 @@ class SystemConfigOAuthDto { @IsString() storageQuotaClaim!: string; + + @IsString() + roleClaim!: string; } class SystemConfigPasswordLoginDto { @@ -408,7 +417,7 @@ class SystemConfigReverseGeocodingDto { } class SystemConfigFacesDto { - @IsBoolean() + @ValidateBoolean() import!: boolean; } @@ -427,12 +436,12 @@ class SystemConfigServerDto { @IsString() loginPageMessage!: string; - @IsBoolean() + @ValidateBoolean() publicUsers!: boolean; } class SystemConfigSmtpTransportDto { - @IsBoolean() + @ValidateBoolean() ignoreCert!: boolean; @IsNotEmpty() @@ -452,7 +461,7 @@ class SystemConfigSmtpTransportDto { } export class SystemConfigSmtpDto { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; @ValidateIf(isEmailNotificationEnabled) @@ -525,8 +534,7 @@ export class SystemConfigThemeDto { } class SystemConfigGeneratedImageDto { - @IsEnum(ImageFormat) - @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) + @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat' }) format!: ImageFormat; @IsInt() @@ -544,13 +552,10 @@ class SystemConfigGeneratedImageDto { } class SystemConfigGeneratedFullsizeImageDto { - @IsBoolean() - @Type(() => Boolean) - @ApiProperty({ type: 'boolean' }) + @ValidateBoolean() enabled!: boolean; - @IsEnum(ImageFormat) - @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) + @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat' }) format!: ImageFormat; @IsInt() @@ -577,8 +582,7 @@ export class SystemConfigImageDto { @IsObject() fullsize!: SystemConfigGeneratedFullsizeImageDto; - @IsEnum(Colorspace) - @ApiProperty({ enumName: 'Colorspace', enum: Colorspace }) + @ValidateEnum({ enum: Colorspace, name: 'Colorspace' }) colorspace!: Colorspace; @ValidateBoolean() @@ -635,6 +639,11 @@ export class SystemConfigDto implements SystemConfig { @IsObject() newVersionCheck!: SystemConfigNewVersionCheckDto; + @Type(() => SystemConfigNightlyTasksDto) + @ValidateNested() + @IsObject() + nightlyTasks!: SystemConfigNightlyTasksDto; + @Type(() => SystemConfigOAuthDto) @ValidateNested() @IsObject() diff --git a/server/src/dtos/system-metadata.dto.ts b/server/src/dtos/system-metadata.dto.ts index c8e64f2300..0005aee7eb 100644 --- a/server/src/dtos/system-metadata.dto.ts +++ b/server/src/dtos/system-metadata.dto.ts @@ -1,11 +1,12 @@ -import { IsBoolean } from 'class-validator'; +import { ValidateBoolean } from 'src/validation'; export class AdminOnboardingUpdateDto { - @IsBoolean() + @ValidateBoolean() isOnboarded!: boolean; } export class AdminOnboardingResponseDto { + @ValidateBoolean() isOnboarded!: boolean; } diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index af2eae7e72..449cec3207 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsString } from 'class-validator'; +import { IsString } from 'class-validator'; import { AssetOrder, AssetVisibility } from 'src/enum'; -import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation'; +import { ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; export class TimeBucketDto { @ValidateUUID({ optional: true, description: 'Filter assets by specific user ID' }) @@ -38,16 +38,17 @@ export class TimeBucketDto { @ValidateBoolean({ optional: true, description: 'Include assets shared by partners' }) withPartners?: boolean; - @IsEnum(AssetOrder) - @Optional() - @ApiProperty({ + @ValidateEnum({ enum: AssetOrder, - enumName: 'AssetOrder', + name: 'AssetOrder', description: 'Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)', + optional: true, }) order?: AssetOrder; - @ValidateAssetVisibility({ + @ValidateEnum({ + enum: AssetVisibility, + name: 'AssetVisibility', optional: true, description: 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)', }) @@ -93,10 +94,10 @@ export class TimeBucketAssetResponseDto { }) isFavorite!: boolean[]; - @ApiProperty({ + @ValidateEnum({ enum: AssetVisibility, - enumName: 'AssetVisibility', - isArray: true, + name: 'AssetVisibility', + each: true, description: 'Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)', }) visibility!: AssetVisibility[]; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 6765df9f73..b258158ae2 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -1,14 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator'; +import { IsDateString, IsInt, IsPositive, ValidateNested } from 'class-validator'; import { AssetOrder, UserAvatarColor } from 'src/enum'; import { UserPreferences } from 'src/types'; -import { Optional, ValidateBoolean } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; class AvatarUpdate { - @Optional() - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true }) color?: UserAvatarColor; } @@ -23,8 +21,7 @@ class RatingsUpdate { } class AlbumsUpdate { - @IsEnum(AssetOrder) - @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true }) defaultAssetOrder?: AssetOrder; } @@ -159,9 +156,8 @@ export class UserPreferencesUpdateDto { } class AlbumsResponse { - @IsEnum(AssetOrder) - @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) - defaultAssetOrder: AssetOrder = AssetOrder.DESC; + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' }) + defaultAssetOrder: AssetOrder = AssetOrder.Desc; } class RatingsResponse { diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index ed08f7534d..0da86bfcb5 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,10 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator'; +import { IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator'; import { User, UserAdmin } from 'src/database'; import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; import { UserMetadataItem } from 'src/types'; -import { Optional, PinCode, ValidateBoolean, ValidateUUID, toEmail, toSanitized } from 'src/validation'; +import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation'; export class UserUpdateMeDto { @Optional() @@ -23,9 +23,7 @@ export class UserUpdateMeDto { @IsNotEmpty() name?: string; - @Optional({ nullable: true }) - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true }) avatarColor?: UserAvatarColor | null; } @@ -34,7 +32,7 @@ export class UserResponseDto { name!: string; email!: string; profileImagePath!: string; - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor' }) avatarColor!: UserAvatarColor; profileChangedAt!: Date; } @@ -84,9 +82,7 @@ export class UserAdminCreateDto { @IsString() name!: string; - @Optional({ nullable: true }) - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true }) avatarColor?: UserAvatarColor | null; @Optional({ nullable: true }) @@ -103,12 +99,10 @@ export class UserAdminCreateDto { @ValidateBoolean({ optional: true }) shouldChangePassword?: boolean; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) notify?: boolean; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) isAdmin?: boolean; } @@ -131,9 +125,7 @@ export class UserAdminUpdateDto { @IsNotEmpty() name?: string; - @Optional({ nullable: true }) - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true }) avatarColor?: UserAvatarColor | null; @Optional({ nullable: true }) @@ -150,8 +142,7 @@ export class UserAdminUpdateDto { @ApiProperty({ type: 'integer', format: 'int64' }) quotaSizeInBytes?: number | null; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) isAdmin?: boolean; } @@ -172,7 +163,7 @@ export class UserAdminResponseDto extends UserResponseDto { quotaSizeInBytes!: number | null; @ApiProperty({ type: 'integer', format: 'int64' }) quotaUsageInBytes!: number | null; - @ApiProperty({ enumName: 'UserStatus', enum: UserStatus }) + @ValidateEnum({ enum: UserStatus, name: 'UserStatus' }) status!: string; license!: UserLicense | null; } @@ -180,7 +171,7 @@ export class UserAdminResponseDto extends UserResponseDto { export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { const metadata = entity.metadata || []; const license = metadata.find( - (item): item is UserMetadataItem => item.key === UserMetadataKey.LICENSE, + (item): item is UserMetadataItem => item.key === UserMetadataKey.License, )?.value; return { ...mapUser(entity), diff --git a/server/src/emails/components/button.component.tsx b/server/src/emails/components/button.component.tsx index 9c229fc16d..b490e36650 100644 --- a/server/src/emails/components/button.component.tsx +++ b/server/src/emails/components/button.component.tsx @@ -2,9 +2,7 @@ import React from 'react'; import { Button, ButtonProps } from '@react-email/components'; -interface ImmichButtonProps extends ButtonProps {} - -export const ImmichButton = ({ children, ...props }: ImmichButtonProps) => ( +export const ImmichButton = ({ children, ...props }: ButtonProps) => ( - - -{/if} diff --git a/web/src/lib/components/user-settings-page/device-list.svelte b/web/src/lib/components/user-settings-page/device-list.svelte index a56f4cc316..4635913c24 100644 --- a/web/src/lib/components/user-settings-page/device-list.svelte +++ b/web/src/lib/components/user-settings-page/device-list.svelte @@ -1,7 +1,6 @@ -
+
import { dateFormats } from '$lib/constants'; - import { modalManager } from '$lib/managers/modal-manager.svelte'; import ApiKeyModal from '$lib/modals/ApiKeyModal.svelte'; import ApiKeySecretModal from '$lib/modals/ApiKeySecretModal.svelte'; import { locale } from '$lib/stores/preferences.store'; import { createApiKey, deleteApiKey, getApiKeys, updateApiKey, type ApiKeyResponseDto } from '@immich/sdk'; - import { Button, IconButton } from '@immich/ui'; + import { Button, IconButton, modalManager } from '@immich/ui'; import { mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; diff --git a/web/src/lib/components/user-settings-page/user-purchase-settings.svelte b/web/src/lib/components/user-settings-page/user-purchase-settings.svelte index 2690559d89..69825dcfda 100644 --- a/web/src/lib/components/user-settings-page/user-purchase-settings.svelte +++ b/web/src/lib/components/user-settings-page/user-purchase-settings.svelte @@ -5,7 +5,6 @@ import PurchaseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import { dateFormats } from '$lib/constants'; - import { modalManager } from '$lib/managers/modal-manager.svelte'; import { locale } from '$lib/stores/preferences.store'; import { purchaseStore } from '$lib/stores/purchase.store'; import { preferences, user } from '$lib/stores/user.store'; @@ -20,7 +19,7 @@ isHttpError, type LicenseResponseDto, } from '@immich/sdk'; - import { Button } from '@immich/ui'; + import { Button, modalManager } from '@immich/ui'; import { mdiKey } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index 32747f5ba6..ac8f3432b7 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -22,7 +22,7 @@ mdiFeatureSearchOutline, mdiKeyOutline, mdiLockSmart, - mdiOnepassword, + mdiFormTextboxPassword, mdiServerOutline, mdiTwoFactorAuthentication, } from '@mdi/js'; @@ -124,7 +124,12 @@ {/if} - + diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 1a40f8522e..b354989e17 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -11,6 +11,7 @@ export enum AssetAction { UNSTACK = 'unstack', KEEP_THIS_DELETE_OTHERS = 'keep-this-delete-others', SET_STACK_PRIMARY_ASSET = 'set-stack-primary-asset', + REMOVE_ASSET_FROM_STACK = 'remove-asset-from-stack', SET_VISIBILITY_LOCKED = 'set-visibility-locked', SET_VISIBILITY_TIMELINE = 'set-visibility-timeline', } diff --git a/web/src/lib/managers/modal-manager.svelte.ts b/web/src/lib/managers/modal-manager.svelte.ts deleted file mode 100644 index f5612d072d..0000000000 --- a/web/src/lib/managers/modal-manager.svelte.ts +++ /dev/null @@ -1,53 +0,0 @@ -import ConfirmModal from '$lib/modals/ConfirmModal.svelte'; -import { mount, unmount, type Component, type ComponentProps } from 'svelte'; - -type OnCloseData = T extends { onClose: (data?: infer R) => void } - ? R | undefined - : T extends { onClose: (data: infer R) => void } - ? R - : never; -type ExtendsEmptyObject = keyof T extends never ? never : T; -type StripValueIfOptional = T extends undefined ? undefined : T; - -// if the modal does not expect any props, makes the props param optional but also allows passing `{}` and `undefined` -type OptionalParamIfEmpty = ExtendsEmptyObject extends never ? [] | [Record | undefined] : [T]; - -class ModalManager { - show(Component: Component, ...props: OptionalParamIfEmpty>) { - return this.open(Component, ...props).onClose; - } - - open>( - Component: Component, - ...props: OptionalParamIfEmpty> - ) { - let modal: object = {}; - let onClose: (...args: [StripValueIfOptional]) => Promise; - - const deferred = new Promise>((resolve) => { - onClose = async (...args: [StripValueIfOptional]) => { - await unmount(modal); - resolve(args?.[0]); - }; - - modal = mount(Component, { - target: document.body, - props: { - ...((props?.[0] ?? {}) as T), - onClose, - }, - }); - }); - - return { - onClose: deferred, - close: (...args: [StripValueIfOptional]) => onClose(args[0]), - }; - } - - showDialog(options: Omit, 'onClose'>) { - return this.show(ConfirmModal, options); - } -} - -export const modalManager = new ModalManager(); diff --git a/web/src/lib/managers/timeline-manager/day-group.svelte.ts b/web/src/lib/managers/timeline-manager/day-group.svelte.ts index 2a949499ec..9d5008bf83 100644 --- a/web/src/lib/managers/timeline-manager/day-group.svelte.ts +++ b/web/src/lib/managers/timeline-manager/day-group.svelte.ts @@ -4,6 +4,7 @@ import type { CommonLayoutOptions } from '$lib/utils/layout-utils'; import { getJustifiedLayoutFromAssets, getPosition } from '$lib/utils/layout-utils'; import { plainDateTimeCompare } from '$lib/utils/timeline-util'; +import { SvelteSet } from 'svelte/reactivity'; import type { MonthGroup } from './month-group.svelte'; import type { AssetOperation, Direction, MoveAsset, TimelineAsset } from './types'; import { ViewerAsset } from './viewer-asset.svelte'; @@ -109,13 +110,13 @@ export class DayGroup { if (ids.size === 0) { return { moveAssets: [] as MoveAsset[], - processedIds: new Set(), + processedIds: new SvelteSet(), unprocessedIds: ids, changedGeometry: false, }; } - const unprocessedIds = new Set(ids); - const processedIds = new Set(); + const unprocessedIds = new SvelteSet(ids); + const processedIds = new SvelteSet(); const moveAssets: MoveAsset[] = []; let changedGeometry = false; for (const assetId of unprocessedIds) { diff --git a/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts b/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts index 66cca61d45..e511df9bf0 100644 --- a/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts +++ b/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts @@ -1,5 +1,6 @@ import { setDifference, type TimelinePlainDate } from '$lib/utils/timeline-util'; import { AssetOrder } from '@immich/sdk'; +import { SvelteSet } from 'svelte/reactivity'; import type { DayGroup } from './day-group.svelte'; import type { MonthGroup } from './month-group.svelte'; import type { TimelineAsset } from './types'; @@ -9,8 +10,8 @@ export class GroupInsertionCache { [year: number]: { [month: number]: { [day: number]: DayGroup } }; } = {}; unprocessedAssets: TimelineAsset[] = []; - changedDayGroups = new Set(); - newDayGroups = new Set(); + changedDayGroups = new SvelteSet(); + newDayGroups = new SvelteSet(); getDayGroup({ year, month, day }: TimelinePlainDate): DayGroup | undefined { return this.#lookupCache[year]?.[month]?.[day]; @@ -31,7 +32,7 @@ export class GroupInsertionCache { } get updatedBuckets() { - const updated = new Set(); + const updated = new SvelteSet(); for (const group of this.changedDayGroups) { updated.add(group.monthGroup); } @@ -39,7 +40,7 @@ export class GroupInsertionCache { } get bucketsWithNewDayGroups() { - const updated = new Set(); + const updated = new SvelteSet(); for (const group of this.newDayGroups) { updated.add(group.monthGroup); } diff --git a/web/src/lib/managers/timeline-manager/internal/operations-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/operations-support.svelte.ts index 82ec78499b..4419de2103 100644 --- a/web/src/lib/managers/timeline-manager/internal/operations-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/operations-support.svelte.ts @@ -1,6 +1,7 @@ import { setDifference, type TimelinePlainDate } from '$lib/utils/timeline-util'; import { AssetOrder } from '@immich/sdk'; +import { SvelteSet } from 'svelte/reactivity'; import { GroupInsertionCache } from '../group-insertion-cache.svelte'; import { MonthGroup } from '../month-group.svelte'; import type { TimelineManager } from '../timeline-manager.svelte'; @@ -18,7 +19,7 @@ export function addAssetsToMonthGroups( } const addContext = new GroupInsertionCache(); - const updatedMonthGroups = new Set(); + const updatedMonthGroups = new SvelteSet(); const monthCount = timelineManager.months.length; for (const asset of assets) { let month = getMonthGroupByDate(timelineManager, asset.localDateTime); @@ -63,12 +64,12 @@ export function runAssetOperation( options: { order: AssetOrder }, ) { if (ids.size === 0) { - return { processedIds: new Set(), unprocessedIds: ids, changedGeometry: false }; + return { processedIds: new SvelteSet(), unprocessedIds: ids, changedGeometry: false }; } - const changedMonthGroups = new Set(); - let idsToProcess = new Set(ids); - const idsProcessed = new Set(); + const changedMonthGroups = new SvelteSet(); + let idsToProcess = new SvelteSet(ids); + const idsProcessed = new SvelteSet(); const combinedMoveAssets: { asset: TimelineAsset; date: TimelinePlainDate }[][] = []; for (const month of timelineManager.months) { if (idsToProcess.size > 0) { diff --git a/web/src/lib/managers/timeline-manager/internal/search-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/search-support.svelte.ts index f86e2c1501..f85246933f 100644 --- a/web/src/lib/managers/timeline-manager/internal/search-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/search-support.svelte.ts @@ -1,4 +1,5 @@ import { plainDateTimeCompare, type TimelinePlainYearMonth } from '$lib/utils/timeline-util'; +import { AssetOrder } from '@immich/sdk'; import type { MonthGroup } from '../month-group.svelte'; import type { TimelineManager } from '../timeline-manager.svelte'; import type { AssetDescriptor, Direction, TimelineAsset } from '../types'; @@ -113,11 +114,10 @@ export async function retrieveRange(timelineManager: TimelineManager, start: Ass if (!endMonthGroup || !endAsset) { return []; } - let direction: Direction = 'earlier'; - if (plainDateTimeCompare(true, startAsset.localDateTime, endAsset.localDateTime) < 0) { + const assetOrder: AssetOrder = timelineManager.getAssetOrder(); + if (plainDateTimeCompare(assetOrder === AssetOrder.Desc, startAsset.localDateTime, endAsset.localDateTime) < 0) { [startAsset, endAsset] = [endAsset, startAsset]; [startMonthGroup, endMonthGroup] = [endMonthGroup, startMonthGroup]; - direction = 'earlier'; } const range: TimelineAsset[] = []; @@ -126,7 +126,6 @@ export async function retrieveRange(timelineManager: TimelineManager, start: Ass startMonthGroup, startDayGroup, startAsset, - direction, })) { range.push(targetAsset); if (targetAsset.id === endAsset.id) { diff --git a/web/src/lib/managers/timeline-manager/month-group.svelte.ts b/web/src/lib/managers/timeline-manager/month-group.svelte.ts index bbcfe88caa..9f7112963a 100644 --- a/web/src/lib/managers/timeline-manager/month-group.svelte.ts +++ b/web/src/lib/managers/timeline-manager/month-group.svelte.ts @@ -17,6 +17,7 @@ import { import { t } from 'svelte-i18n'; import { get } from 'svelte/store'; +import { SvelteSet } from 'svelte/reactivity'; import { DayGroup } from './day-group.svelte'; import { GroupInsertionCache } from './group-insertion-cache.svelte'; import type { TimelineManager } from './timeline-manager.svelte'; @@ -115,15 +116,15 @@ export class MonthGroup { if (ids.size === 0) { return { moveAssets: [] as MoveAsset[], - processedIds: new Set(), + processedIds: new SvelteSet(), unprocessedIds: ids, changedGeometry: false, }; } const { dayGroups } = this; let combinedChangedGeometry = false; - let idsToProcess = new Set(ids); - const idsProcessed = new Set(); + let idsToProcess = new SvelteSet(ids); + const idsProcessed = new SvelteSet(); const combinedMoveAssets: MoveAsset[][] = []; let index = dayGroups.length; while (index--) { diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts index 8aacd0a90a..8a1ce8fe18 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts @@ -6,7 +6,7 @@ import { CancellableTask } from '$lib/utils/cancellable-task'; import { toTimelineAsset, type TimelinePlainDateTime, type TimelinePlainYearMonth } from '$lib/utils/timeline-util'; import { clamp, debounce, isEqual } from 'lodash-es'; -import { SvelteSet } from 'svelte/reactivity'; +import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity'; import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte'; import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte'; @@ -293,7 +293,7 @@ export class TimelineManager { }); this.months = timebuckets.map((timeBucket) => { - const date = new Date(timeBucket.timeBucket); + const date = new SvelteDate(timeBucket.timeBucket); return new MonthGroup( this, { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1 }, @@ -456,14 +456,14 @@ export class TimelineManager { } updateAssetOperation(ids: string[], operation: AssetOperation) { - runAssetOperation(this, new Set(ids), operation, { order: this.#options.order ?? AssetOrder.Desc }); + runAssetOperation(this, new SvelteSet(ids), operation, { order: this.#options.order ?? AssetOrder.Desc }); } updateAssets(assets: TimelineAsset[]) { - const lookup = new Map(assets.map((asset) => [asset.id, asset])); + const lookup = new SvelteMap(assets.map((asset) => [asset.id, asset])); const { unprocessedIds } = runAssetOperation( this, - new Set(lookup.keys()), + new SvelteSet(lookup.keys()), (asset) => { updateObject(asset, lookup.get(asset.id)); return { remove: false }; @@ -480,7 +480,7 @@ export class TimelineManager { removeAssets(ids: string[]) { const { unprocessedIds } = runAssetOperation( this, - new Set(ids), + new SvelteSet(ids), () => { return { remove: true }; }, @@ -540,4 +540,8 @@ export class TimelineManager { isMismatched(this.#options.isTrashed, asset.isTrashed) ); } + + getAssetOrder() { + return this.#options.order ?? AssetOrder.Desc; + } } diff --git a/web/src/lib/managers/upload-manager.svelte.ts b/web/src/lib/managers/upload-manager.svelte.ts new file mode 100644 index 0000000000..0ff2b0c214 --- /dev/null +++ b/web/src/lib/managers/upload-manager.svelte.ts @@ -0,0 +1,24 @@ +import { eventManager } from '$lib/managers/event-manager.svelte'; +import { getSupportedMediaTypes, type ServerMediaTypesResponseDto } from '@immich/sdk'; + +class UploadManager { + mediaTypes = $state({ image: [], sidecar: [], video: [] }); + + constructor() { + eventManager.on('app.init', () => void this.#loadExtensions()); + } + + async #loadExtensions() { + try { + this.mediaTypes = await getSupportedMediaTypes(); + } catch { + console.error('Failed to load supported media types'); + } + } + + getExtensions() { + return [...this.mediaTypes.image, ...this.mediaTypes.video]; + } +} + +export const uploadManager = new UploadManager(); diff --git a/web/src/lib/modals/AlbumOptionsModal.svelte b/web/src/lib/modals/AlbumOptionsModal.svelte index f9c9cab204..e56fe784a6 100644 --- a/web/src/lib/modals/AlbumOptionsModal.svelte +++ b/web/src/lib/modals/AlbumOptionsModal.svelte @@ -4,7 +4,6 @@ import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; - import { modalManager } from '$lib/managers/modal-manager.svelte'; import { handleError } from '$lib/utils/handle-error'; import { AlbumUserRole, @@ -15,7 +14,7 @@ type AlbumResponseDto, type UserResponseDto, } from '@immich/sdk'; - import { Modal, ModalBody } from '@immich/ui'; + import { Modal, ModalBody, modalManager } from '@immich/ui'; import { mdiArrowDownThin, mdiArrowUpThin, mdiDotsVertical, mdiPlus } from '@mdi/js'; import { findKey } from 'lodash-es'; import { t } from 'svelte-i18n'; diff --git a/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte b/web/src/lib/modals/AlbumPickerModal.svelte similarity index 78% rename from web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte rename to web/src/lib/modals/AlbumPickerModal.svelte index ab763546af..3e16e03b80 100644 --- a/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte +++ b/web/src/lib/modals/AlbumPickerModal.svelte @@ -6,12 +6,12 @@ isSelectableRowType, } from '$lib/components/shared-components/album-selection/album-selection-utils'; import { albumViewSettings } from '$lib/stores/preferences.store'; - import { type AlbumResponseDto, getAllAlbums } from '@immich/sdk'; + import { type AlbumResponseDto, createAlbum, getAllAlbums } from '@immich/sdk'; import { Modal, ModalBody } from '@immich/ui'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; - import AlbumListItem from '../../asset-viewer/album-list-item.svelte'; - import NewAlbumListItem from './new-album-list-item.svelte'; + import AlbumListItem from '../components/asset-viewer/album-list-item.svelte'; + import NewAlbumListItem from '../components/shared-components/album-selection/new-album-list-item.svelte'; let albums: AlbumResponseDto[] = $state([]); let recentAlbums: AlbumResponseDto[] = $state([]); @@ -20,13 +20,11 @@ let selectedRowIndex: number = $state(-1); interface Props { - onNewAlbum: (search: string) => void; - onAlbumClick: (album: AlbumResponseDto) => void; shared: boolean; - onClose: () => void; + onClose: (album?: AlbumResponseDto) => void; } - let { onNewAlbum, onAlbumClick, shared, onClose }: Props = $props(); + let { shared, onClose }: Props = $props(); onMount(async () => { albums = await getAllAlbums({ shared: shared || undefined }); @@ -38,7 +36,34 @@ const albumModalRows = $derived(rowConverter.toModalRows(search, recentAlbums, albums, selectedRowIndex)); const selectableRowCount = $derived(albumModalRows.filter((row) => isSelectableRowType(row.type)).length); - const onkeydown = (e: KeyboardEvent) => { + const onNewAlbum = async (name: string) => { + const album = await createAlbum({ createAlbumDto: { albumName: name } }); + onClose(album); + }; + + const onEnter = async () => { + const item = albumModalRows.find(({ selected }) => selected); + if (!item) { + return; + } + + switch (item.type) { + case AlbumModalRowType.NEW_ALBUM: { + await onNewAlbum(search); + break; + } + case AlbumModalRowType.ALBUM_ITEM: { + if (item.album) { + onClose(item.album); + } + break; + } + } + + selectedRowIndex = -1; + }; + + const onkeydown = async (e: KeyboardEvent) => { switch (e.key) { case 'ArrowUp': { e.preventDefault(); @@ -60,15 +85,7 @@ } case 'Enter': { e.preventDefault(); - const selectedRow = albumModalRows.find((row) => row.selected); - if (selectedRow) { - if (selectedRow.type === AlbumModalRowType.NEW_ALBUM) { - onNewAlbum(search); - } else if (selectedRow.type === AlbumModalRowType.ALBUM_ITEM && selectedRow.album) { - onAlbumClick(selectedRow.album); - } - selectedRowIndex = -1; - } + await onEnter(); break; } default: { @@ -76,8 +93,6 @@ } } }; - - const handleAlbumClick = (album: AlbumResponseDto) => () => onAlbumClick(album); @@ -119,7 +134,7 @@ album={row.album} selected={row.selected || false} searchQuery={search} - onAlbumClick={handleAlbumClick(row.album)} + onAlbumClick={() => onClose(row.album)} /> {/if} {/each} diff --git a/web/src/lib/modals/AlbumUsersModal.svelte b/web/src/lib/modals/AlbumUsersModal.svelte index 4f1b8aa94d..32c8cd28cf 100644 --- a/web/src/lib/modals/AlbumUsersModal.svelte +++ b/web/src/lib/modals/AlbumUsersModal.svelte @@ -6,7 +6,6 @@ notificationController, } from '$lib/components/shared-components/notification/notification'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; - import { modalManager } from '$lib/managers/modal-manager.svelte'; import { handleError } from '$lib/utils/handle-error'; import { AlbumUserRole, @@ -16,7 +15,7 @@ type AlbumResponseDto, type UserResponseDto, } from '@immich/sdk'; - import { Modal, ModalBody } from '@immich/ui'; + import { Modal, ModalBody, modalManager } from '@immich/ui'; import { mdiDotsVertical } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; diff --git a/web/src/lib/modals/ApiKeyModal.svelte b/web/src/lib/modals/ApiKeyModal.svelte index 15902c8e53..e0e1197240 100644 --- a/web/src/lib/modals/ApiKeyModal.svelte +++ b/web/src/lib/modals/ApiKeyModal.svelte @@ -5,11 +5,18 @@ } from '$lib/components/shared-components/notification/notification'; import ApiKeyGrid from '$lib/components/user-settings-page/user-api-key-grid.svelte'; import { Permission } from '@immich/sdk'; - import { Button, Checkbox, HStack, Label, Modal, ModalBody, ModalFooter } from '@immich/ui'; - import { mdiKeyVariant } from '@mdi/js'; + import { Button, Checkbox, Field, HStack, IconButton, Input, Label, Modal, ModalBody, ModalFooter } from '@immich/ui'; + import { mdiClose, mdiKeyVariant } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; + const matches = (value: string) => { + value = value.toLowerCase(); + return ([title, items]: [string, Permission[]]) => { + return title.toLowerCase().includes(value) || items.some((item) => item.toLowerCase().includes(value)); + }; + }; + interface Props { apiKey: { name: string; permissions: Permission[] }; title: string; @@ -19,137 +26,26 @@ } let { apiKey = $bindable(), title, cancelText = $t('cancel'), submitText = $t('save'), onClose }: Props = $props(); + let name = $derived(apiKey.name); let selectedItems: Permission[] = $state(apiKey.permissions); let selectAllItems = $derived(selectedItems.length === Object.keys(Permission).length - 1); - const permissions: Map = new Map(); + const permissions: Record = {}; + for (const permission of Object.values(Permission)) { + if (permission === Permission.All) { + continue; + } - permissions.set('activity', [ - Permission.ActivityCreate, - Permission.ActivityRead, - Permission.ActivityUpdate, - Permission.ActivityDelete, - Permission.ActivityStatistics, - ]); + const [group] = permission.split('.'); + if (!permissions[group]) { + permissions[group] = []; + } + permissions[group].push(permission); + } - permissions.set('api_key', [ - Permission.ApiKeyCreate, - Permission.ApiKeyRead, - Permission.ApiKeyUpdate, - Permission.ApiKeyDelete, - ]); - - permissions.set('asset', [ - Permission.AssetRead, - Permission.AssetUpdate, - Permission.AssetDelete, - Permission.AssetShare, - Permission.AssetView, - Permission.AssetDownload, - Permission.AssetUpload, - ]); - - permissions.set('album', [ - Permission.AlbumCreate, - Permission.AlbumRead, - Permission.AlbumUpdate, - Permission.AlbumDelete, - Permission.AlbumStatistics, - - Permission.AlbumAddAsset, - Permission.AlbumRemoveAsset, - Permission.AlbumShare, - Permission.AlbumDownload, - ]); - - permissions.set('auth_device', [Permission.AuthDeviceDelete]); - - permissions.set('archive', [Permission.ArchiveRead]); - - permissions.set('face', [Permission.FaceCreate, Permission.FaceRead, Permission.FaceUpdate, Permission.FaceDelete]); - - permissions.set('library', [ - Permission.LibraryCreate, - Permission.LibraryRead, - Permission.LibraryUpdate, - Permission.LibraryDelete, - Permission.LibraryStatistics, - ]); - - permissions.set('timeline', [Permission.TimelineRead, Permission.TimelineDownload]); - - permissions.set('memory', [ - Permission.MemoryCreate, - Permission.MemoryRead, - Permission.MemoryUpdate, - Permission.MemoryDelete, - ]); - - permissions.set('notification', [ - Permission.NotificationCreate, - Permission.NotificationRead, - Permission.NotificationUpdate, - Permission.NotificationDelete, - ]); - - permissions.set('partner', [ - Permission.PartnerCreate, - Permission.PartnerRead, - Permission.PartnerUpdate, - Permission.PartnerDelete, - ]); - - permissions.set('person', [ - Permission.PersonCreate, - Permission.PersonRead, - Permission.PersonUpdate, - Permission.PersonDelete, - Permission.PersonStatistics, - Permission.PersonMerge, - Permission.PersonReassign, - ]); - - permissions.set('session', [ - Permission.SessionCreate, - Permission.SessionRead, - Permission.SessionUpdate, - Permission.SessionDelete, - Permission.SessionLock, - ]); - - permissions.set('sharedLink', [ - Permission.SharedLinkCreate, - Permission.SharedLinkRead, - Permission.SharedLinkUpdate, - Permission.SharedLinkDelete, - ]); - - permissions.set('stack', [ - Permission.StackCreate, - Permission.StackRead, - Permission.StackUpdate, - Permission.StackDelete, - ]); - - permissions.set('systemConfig', [Permission.SystemConfigRead, Permission.SystemConfigUpdate]); - - permissions.set('systemMetadata', [Permission.SystemMetadataRead, Permission.SystemMetadataUpdate]); - - permissions.set('tag', [ - Permission.TagCreate, - Permission.TagRead, - Permission.TagUpdate, - Permission.TagDelete, - Permission.TagAsset, - ]); - - permissions.set('adminUser', [ - Permission.AdminUserCreate, - Permission.AdminUserRead, - Permission.AdminUserUpdate, - Permission.AdminUserDelete, - ]); + let searchValue = $state(''); + let filteredResults = $derived(Object.entries(permissions).filter(matches(searchValue))); const handleSelectItems = (permissions: Permission[]) => { selectedItems = Array.from(new Set([...selectedItems, ...permissions])); @@ -176,9 +72,9 @@ }); } else { if (selectAllItems) { - onClose({ name: apiKey.name, permissions: [Permission.All] }); + onClose({ name, permissions: [Permission.All] }); } else { - onClose({ name: apiKey.name, permissions: selectedItems }); + onClose({ name, permissions: selectedItems }); } } }; @@ -199,22 +95,37 @@
- - + + +
- -
- -